SegmentFault beanlam最新的文章
2021-08-06T22:48:46+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Java 调试技术 JPDA 架构解读
https://segmentfault.com/a/1190000040469952
2021-08-06T22:48:46+08:00
2021-08-06T22:48:46+08:00
ytbean
https://segmentfault.com/u/ytbean
2
<p>原文链接:<a href="https://link.segmentfault.com/?enc=PU7auycRvhAEWKQ86B5d%2Bg%3D%3D.MXwfu1A%2FLz%2Fk3I9CKOK3%2FweOEZqbMGl9WcIUmbhRACJY3rMzugEZw1YH1fgez9nEcK8weHoZP32bgzfkd7f2CQ%3D%3D" rel="nofollow">《Java 调试技术 JPDA 架构解读》http://www.ytbean.com/posts/java-debug-internals/</a></p><h2>JPDA 概览</h2><p>JPDA 的全称是 Java Platform Debugger Architecture,它是 Java 官方针对 Java 代码调试所设计的一个机制。在 Oracle 官网上有专门的<a href="https://link.segmentfault.com/?enc=kp6vQy1fZSXWrbKBuZEehw%3D%3D.sjwxEVNNpoH7DhPZratGhcyoeWwhokSAuPtfMmmkbHz%2F1trmoCGCAzhFqndqf0ZWU%2F%2BSp3Ix9hcPP9tm3OWzz4AXVSULPNrMxBqQE5pZCwc%3D" rel="nofollow">页面</a>介绍。它属于多层架构,包括:<a href="https://link.segmentfault.com/?enc=xosQd94simF%2BLC4%2BK3c%2BPA%3D%3D.L2BNJMuLm%2BKRKb8EnoG1qxo6A1Ko8cFrb6YwrmZHmdgriUZAlHy3vkzH3VN2qOkeN3wFe7GPqU3QqvPuTuby9w%3D%3D" rel="nofollow">JVMTI 接口规范</a>、<a href="https://link.segmentfault.com/?enc=iGm6225ORkzrUIyw4swgWQ%3D%3D.nlv7wyCM%2FpJsgUKeXMnOWzUEqDXWhoXIRan3%2FyTERvhK6iaj2JOmWNWyEyoxAbtvAiJL8s%2FrXYg5UB2v60sc7ceVtP%2F46jYfrCGqnVhqYrw%3D" rel="nofollow">JDWP 通信规范</a>、<a href="https://link.segmentfault.com/?enc=sVyn12%2B3um6lzAa6yyuzUA%3D%3D.PWXFvve9P91TBv9Sr0OoiO2MP2rH7%2FWMoEmlrcdb9NUe2IKORJqHPcFFfiwtpdiRb%2FXQyZeN72HhsYE2pZYnh9rmwyPAqDUuR7k6%2B5LoQM0%3D" rel="nofollow">JDI API 层</a>。</p><p><img src="/img/remote/1460000040469954" alt="JPDA 组成" title="JPDA 组成"></p><p>Debug 应该是每个程序员都经历过的,日常基于 IDE 的开发中,我们可以给某行代码打上断点,然后以 Debug 模式运行程序,程序运行后会在断点处暂停,这时开发者可以安逸地查看此时各个变量的值,也可以在此时加上更多的断点。</p><ul><li><p>JVMTI</p><p>Java VM Tool Interface 定义的是一系列跟调试相关的接口,由 VM 实现这些接口。我们所说的程序暂停下来,确实是 JVM 运行代码的时候停在了有断点的地方,那么 JVM 必然提供一种联络方式,让别人告诉它断点在哪个类的第几行。这里的联络方式,就是 JVMTI(JVM Tool Interface),这是 JVM 提供的一个类似钩子的机制,通过 JVMTI,可以指挥 JVM 执行某些操作,例如停在断点处,也可以在 JVM 运行过程中,发生某些事件时,通过钩子通知外部感兴趣的人。</p><p>那么谁可以跟钩子通信呢,并不是说任何人只要有兴趣就行的。JVM 要求它必须是一个 JVMTI Agent,Java 在不同的操作系统中,都已经内置了本地 JVMTI Agent。在 Windows 系统中,这个 JVMTI Agent 就是一个 DLL 文件,在类 Unix 的操作系统中,则是一个 SO 文件。JVMTI Agent 与 JVM 是运行在同一个机器上同一个进程内的。</p></li><li><p>JDWP</p><p>当想要利用 JVMTI 让 JVM 做一些事的时候,那么就要先要与 Agent 通信,由它代为传话。因此,JVMTI Agent 内置了一个称之为“通信后端”的模块,用来接收外部的请求。</p><p>想要与 JVMTI Agent 通信的第三方,就要先与通信后端通信,通信就意味着必然有一个通信协议的存在,这个协议就是 JDWP(Java Debug Wire Protocol) 协议。</p></li><li><p>JDI</p><p>Java 程序员经常使用的 eclipse、idea 这样的 IDE 来 debug 程序的时候,就是以 JDWP 协议与目标 JVM 的 JVMTI Agent 通信的。考虑到 JDWP 协议的实现比较繁琐,Java 官方也在 com.sun.jdi 这个 package 中实现了一个叫做 JDI(Java Debug Interface)基础库,JDI 实现了 JDWP 协议,将与 JVMTI Agent 通信的细节封装为一个又一个 Java API,方便第三方与 JVMTI Agent 通信,与 JVMTI Agent 的通信后端相对应,JDI 包含了一个通信前端模块,负责 JDWP 协议的转换以及消息的发送和接收。</p></li></ul><h3>三层模型</h3><p>JPDA 抽象机制设计为三层:</p><ol><li>第一层:调试方,由 JDI 定义调试方的 API</li><li>第二层:通信层,由 JDWP 定义通信协议规范</li><li>第三层:被调试方,JVMTI 定义如何与目标 JVM 交互</li></ol><p><img src="/img/remote/1460000040469955" alt="JPDA 模块间交互" title="JPDA 模块间交互"></p><p>为什么 JPDA 机制需要设计层三层呢?原因有几个:</p><ol><li>调试方可能在远程进行调试,而 JDWP 协议又是一个非常底层的二进制协议,实现起来需要花费大量的成本。所以通过 JDI 对 JDWP 协议进行实现,并对外提供 API。</li><li>JDI 不仅仅是实现了 JDWP 协议那么简单,它还实现了队列、缓存、连接初始化等等服务,这些服务都可以简单地通过 JDI 的 API 来使用。</li><li>有了 JVMTI,调试就可以与具体的 JVM 解耦,不同类型的 JVM 只要遵循 JVMTI 规范即可,JDWP 不需要假设它正在与某种类型的 JVM 通信。</li></ol><h3>集成选择</h3><p>我们可以不通过 JDI,直接自己实现 JDWP 协议与 JVMTI 通信吗,当然可以。</p><p>我们可以不通过 JDWP 协议,直接在 JVM 进程中,编写本地 C/C++ 代码与 JVMTI Agent,或者自己实现一个 JVMTI Agent 与目标 JVM 通信吗?当然也是可以的。</p><p>这都要从需求出发:</p><ol><li>如果我们只需要实现一个调试器,例如 IDE,那么我们直接使用 JDI 即可。</li><li>如果我们的调试器不是用 Java 语言写的,那么,我们需要自行实现 JDWP 协议。</li><li>如果 JDI/JDWP 所包含的功能不满足我们的需求,例如堆栈分析,那么我们可以直接通过 JVMTI 来实现我们想要的功能。</li></ol><p>JVMTI 规范所对应的功能是最完整的,而 JDWP 仅支持部分功能,JDI 则仅支持调试相关的功能。在功能上,是父子集的关系。</p><h3>通信机制</h3><p>调试器与被调试JVM之间需要通过一定的方式进行通信,通信的机制主要包括两部分</p><ol><li>连接器(Connector)</li><li>通信方式(Transport)</li></ol><p>连接器是指调试器与被调试 JVM 之间的一个连接,JPDA 在 JDI 这一层面实现了连接器。</p><p>通信方式是指调试器与被调试 JVM 之间的数据交换方式和通信报文格式,JPDA 在 JDWP 中定义了报文规范。</p><h4>连接器</h4><p>连接器有三种:</p><ol><li>Listening:调试器监听来自被调试 JVM 的连接;</li><li>Attaching:调试器连接上一个已经处于运行状态的被调试 JVM;</li><li>Launching:调试器直接亲手启动被调试 JVM,此时调试器与被调试代码实际上是运行在同一个 JVM 中的;</li></ol><h4>通信方式</h4><p>调试器与被调试 JVM 之间的数据交换方式,有两种:</p><ol><li><p>基于 Socket 网络连接,主要用于远程调试,即调试器和被调试 JVM 不在同一台机器上;</p><p><img src="/img/remote/1460000040469956" alt="基于 Socket 的网络连接" title="基于 Socket 的网络连接"></p></li></ol><ol start="2"><li><p>基于操作系统共享内存的通信,主要用于调试器和被调试 JVM 在同一台机器上的情况;</p><p><img src="/img/remote/1460000040469957" alt="基于共享内存的连接" title="基于共享内存的连接"></p></li></ol><h3>配置</h3><p>调试器和被调试 JVM 在启动的时候, 都需要通过设置 JVM 参数来让它具有调试的能力或者可被调试的能力。</p><p>对于 JDK5 及以上的版本,参数格式为:<code>-agentlib:jdwp={子配置项}</code></p><p>对于 JDK5 以前的版本,参数格式为:<code>-Xdebug</code> 以及 <code>-Xrunjdwp:{子配置项}</code>。</p><p>而子配置项,包括:</p><ol><li>transport:数据交换方式,可选:<code>dt_socket</code> 和 <code>dt_shmem</code>,分别代表 socket 网络通信和共享内存通信</li><li>Address:标识一个对端的地址,格式为:<code>{ip}:{port}</code></li><li>server:标识自己是调试者还是被调试者,调试者配置为:<code>n</code>,被调试着配置为:<code>y</code></li><li>suspend:只有被调试者才需要配这个参数,当配置为 <code>y</code> 的时候,代表等待调试者连接上来才真正启动 Java 应用;配置为 <code>n</code> 时,则直接启动 Java 应用。</li></ol><blockquote>这里的 Java 应用,是相对于 JVM 来说的,假如把 JVM 看成一个平台,那我们写的代码就是一个 Java 应用。JVM 已经启动,但我们的应用代码还没有跑起来,这种情况在上文的语境中,我们叫做 Java 应用还没启动。</blockquote><p>配置示例:</p><ol><li><p>被调试者开启远程调试监听:</p><pre><code class="bash">-agentlib:jdwp=transport=dt_socket,address=localhost:7007,server=y,suspend=y</code></pre></li><li><p>被调试者开启本地共享内存调试监听:</p><pre><code class="bash">-agentlib:jdwp=transport=dt_shmem,server=y,suspend=n</code></pre></li><li><p>调试者远程连接被调试者:</p><pre><code class="bash">-agentlib:jdwp=transport=dt_socket,address=localhost:7007,server=n,suspend=y</code></pre></li><li><p>调试者基于共享内存方式连接被调试者:</p><pre><code class="bash">-agentlib:jdwp=transport=dt_shmem, address=<mysharedmem></code></pre></li><li><p>调试者基于共享内存方式启动被调试者:</p><pre><code class="bash">-agentlib:jdwp=transport=dt_shmem,server=y,onuncaught=y,launch=d:\bin\debugstub.exe</code></pre></li></ol><blockquote>被调试者基于共享内存的监听启动后,共享内存地址将会打印到控制台上。调试者配置时需要配置这个共享内存的地址</blockquote><h2>JDI</h2><h3>功能</h3><ol><li>提供了跟调试相关的 Java API;</li><li>能够获取一个正在运行的 JVM 的状态,包括:类,数组,接口,基本类型以及这些类型的对象数量;</li><li>与执行相关的控制,例如暂停和恢复线程;</li><li>设置断点,监听异常的发生、类加载、线程创建等;</li><li>提供不同的连接器实现,例如基于 socket 的远程连接器和基于共享内存的本地连接器;</li></ol><h3>技术架构</h3><p><img src="/img/remote/1460000040469958" alt="JDI技术架构" title="JDI技术架构"></p><ol><li>提供事件机制</li><li>对 JDWP 协议的编解码</li></ol><h3>用法</h3><p>要使用 JDI 的功能,需要依赖 JDK 自带的 <code>tools.jar</code> 这个工具包,JDI 相关的代码处于 <code>com.sun.jdi</code> 这个包下面。</p><p>一个大致的使用步骤如下所示:</p><ol><li>获取一个 <code>VirtualMachine</code> 实例</li><li>从 <code>VirtualMachine</code> 实例中获取一个 <code>Connector</code></li><li>使用 <code>VirtualMachine</code> 的 <code>EventRequestManager</code> 来监听我们感兴趣的事件</li></ol><p>事件机制代码示例:</p><pre><code class="java">EventRequestManager em=vm.eventRequestManager();
MethodEntryRequest meR=em.createMethodEntryRequest();
meR.addClassFilter("mypckg.*");
meR.enable();
EventQueue eventQ=vm.eventQueue();
while (running) {
EventSet eventSet=null;
eventSet=eventQ.remove();
EventIterator eventIterator=eventSet.eventIterator();
while (eventIterator.hasNext()) {
Event event=eventIterator.nextEvent();
if (event instanceof MethodEntryEvent) {
// process this event
}
vm.resume();
}
}</code></pre><h2>JDWP</h2><h3>报文格式</h3><p>请求报文:</p><p><img src="/img/remote/1460000040469959" alt="JDWP请求报文格式" title="JDWP请求报文格式"></p><p>响应报文:</p><p><img src="/img/remote/1460000040469960" alt="JDWP响应报文格式" title="JDWP响应报文格式"></p><h3>命令集</h3><table><thead><tr><th>命名集</th><th>命令</th></tr></thead><tbody><tr><td>Virtual Machine</td><td>Version, ClassesBySignature, Suspend, Resume etc</td></tr><tr><td>Reference Type</td><td>Signature, ClassLoader, Fields, Methods etc</td></tr><tr><td>Class Type</td><td>Super Class, Set Values, Invoke Method, NewInstance</td></tr><tr><td>Array Type</td><td>New Instance</td></tr><tr><td>Interface Type</td><td> </td></tr><tr><td>Method</td><td>Line Table, Variable Table, Byte Codes, IsObsolete etc</td></tr><tr><td>Field</td><td> </td></tr><tr><td>Object Reference</td><td>Reference Type, Get Values, Set Values, Monitor Info etc</td></tr><tr><td>String Reference</td><td>Value</td></tr><tr><td>Thread Reference</td><td>Name, Suspend, Resume, Status, Thread Group, Frames etc</td></tr><tr><td>Thread Group Reference</td><td>Name, Parent, Childern</td></tr><tr><td>Etc</td><td> </td></tr></tbody></table><h2>JVMTI</h2><p>JVMTI 从 Java 5 开始引进,替代了 JVMDI 和 JVMPI,JVMDI 已经在 Java6 中移除,Java7 将会移除 JVMPI;</p><h3>接口定义</h3><p>JVMTI 定义了 JVM 必须实现的一系列用于调试的接口,这些接口总体上包含:</p><ol><li>获取信息类的接口,例如获取当前堆内存的使用率</li><li>某种动作,例如设置断点</li><li>通知,例如当一个断点命中时,通知监听者</li></ol><h3>Agent</h3><ol><li>Agent 可以用任何具有调用 C 语言或 C++ 语言能力的语言写,例如 Java。</li><li>函数、事件、数据类型、常量定义等定义在了基础库 jvmti.h</li><li>Agent 跟目标 JVM 是运行在同一个进程的</li><li>允许多个 Agent 并行运行,每个 Agent 相互独立</li><li>JDK 本身已经自带一个调试 Agent,在 windows 下以 JDWP.dll 形式存在,在 linux 下以 JDWP.so 形式存在</li></ol><p>Agent 运行时序图:</p><p><img src="/img/remote/1460000040469961" alt="Agent运行时序图" title="Agent运行时序图"></p><p>JVM 启动的时候,会调用各个 Agent 的启动函数,如果 Agent 启动了, <code>Agent_OnLoad</code> 回调函数会被调起。如果 Agent 是中途才 attach 进 JVM 的,那么回调函数是 <code>Agent_OnAttach</code>。</p><p>当 Agent 将要被关闭的时候,回调函数<code>Agent_OnUnload</code> 会被调起。</p><p>通过配置 JVM 参数的方式,让 JVM 加载 Agent</p><ol><li>-agentlib:{agent-lib-name}={其它配置项}。例如,配置为:<code>-agentlib:myagent</code>,在 windows 平台上,将会搜索 PATH 下的 myagent.dll 文件,在类 Unix 平台上,将会搜索 LD_LIBRARY_PATH 下的 myagent.so 文件。</li><li>-agentpath:{path-to-agent}={其它配置项}。这个配置方式用来配置 Agent 的绝对路径,例如:<code>-agentpath:d:\myagent\MyAgent.dll</code></li></ol><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
JDBC 4.2 Specifications 中文翻译 -- 第十二章 分布式事务
https://segmentfault.com/a/1190000040174839
2021-06-15T10:42:01+08:00
2021-06-15T10:42:01+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>到目前为止,对于事务的讨论基本上都聚焦在本地事务上,本地事务只会涉及到一个单一的数据源。本章开始介绍分布式事务,分布式事务会在单个事务内涉及多个数据源。以下内容主要包括:</p><ul><li>分布识事务基础设施</li><li>事务管理器和资源管理器</li><li><code>XADataSource</code>,<code>XAConnection</code> 和 <code>XAResource</code> 接口</li><li>两阶段提交</li></ul><p>JDBC 的事务管理 API 与 JTA 规范是兼容的。</p><h2>基础设施</h2><p>分布式事务的基础设施包括以下几个部分:</p><ul><li>事务管理器,用来控制事务的边界和管理两阶段提交协议。它也应该是 JTA 的一个经典实现。</li><li>实现了<code>XADataSource</code>,<code>XAConnection</code> 和 <code>XAResource</code> 接口的 JDBC 驱动。</li><li>一个对于应用层完全可见的 <code>DataSource</code> 实现,利用它来操作 <code>XADataSource</code> ,并与事务管理器交互。通常这个实现由应用服务器提供。</li><li>用来管理底层数据的资源管理器。在 JDBC 的语境中,资源管理器指的是数据库服务器。“资源管理器” 这个术语实际上来自于 JTA,在这里使用这个术语是为了强调 JDBC 中的分布式事务是遵循 JTA 规范的架构来处理的。</li></ul><p>通常会以“三层架构”来实现这个基础设施,包括:</p><ol><li>客户端</li><li>一个包含应用程序、EJB 服务器、JDBC 驱动集合的中间层</li><li>多个资源管理器</li></ol><p>分布式事务也可以实现为“两层架构”。在两层架构中,应用层本身就会扮演事务管理器的角色,并且直接操作 <code>XADataSource</code> API。下图阐述了分布式事务的基础设施:</p><p><img src="/img/remote/1460000040174841" alt="image-20210613150204476" title="image-20210613150204476"></p><p>后续的内容将会对基础设施的各个部分进行详细的说明</p><h2><code>XADataSource</code> 和 <code>XAConnection</code></h2><p><code>XADataSource</code> 和 <code>XAConnection</code> 接口,定义在 javax.sql 包中。支持分布式事务的数据库驱动需要实现这两个接口。<code>XAConnection</code> 继承了 <code>PooledConnection</code> 接口,添加了一个 <code>getXAResource</code> 方法,这个方法会生成一个 <code>XAResource</code> 对象,事务管理器可以利用这个对象来完成分布式事务。以下是 <code>XAConnection</code> 接口的定义:</p><pre><code class="java">public interface XAConnection extends PooledConnection {
javax.transaction.xa.XAResource getXAResource()
throws SQLException;
}</code></pre><p>因为继承了 <code>PooledConnection</code> 接口,所以所有的 <code>XAConnection</code> 对象也支持 <code>PooledConnection</code> 中定义的方法。这些对象代表着与底层数据源的一条可重用的物理连接,应用层也可以通过这个对象操作这条连接。</p><p><code>XAConnection</code> 对象由 <code>XADataSource</code> 生成。<code>ConnectionPoolDataSource</code> 和 <code>XADataSource</code> 有一些相似的地方,他们都实现了 <code>DataSource</code> 接口。这样就允许分布式事务的底层实现对于应用层来说是透明的。<code>XADataSource</code> 接口定义如下:</p><pre><code class="java">public interface XADataSource {
XAConnection getXAConnection() throws SQLException;
XAConnection getXAConnection(String user,
String password) throws SQLException;
//...
}</code></pre><p>通常,一个基于 <code>XADataSource</code> 之上的 <code>DataSource</code> 实现,也会包含一个连接池化的模块。</p><h3>部署 <code>XADataSource</code> 对象</h3><p>部署一个 <code>XADataSource</code> 与先前所提到的 <code>ConnectionPoolDataSource</code> 是一样的。流程总共分两步,如下代码所示:</p><pre><code class="java">// com.acme.jdbc.XADataSource implements the
// XADataSource interface.
// Create an instance and set properties.
com.acme.jdbc.XADataSource xads = new com.acme.jdbc.XADataSource();
xads.setServerName(“bookstore”);
xads.setDatabaseName(“bookinventory”);
xads.setPortNumber(9040);
xads.setDescription(“XADataSource for inventory”);
// First register xads with a JNDI naming service, using the
// logical name “jdbc/xa/inventory_xa”
Context ctx = new InitialContext();
ctx.bind(“jdbc/xa/inventory_xa”, xads);
// Next register the overlying DataSource object for application
// access. com.acme.appserver.DataSource is an implementation of
// the DataSource interface.
// Create an instance and set properties.
com.acme.appserver.DataSource ds =
new com.acme.appserver.DataSource();
ds.setDescription(“Datasource supporting distributed transactions”);
// Reference the previously registered XADataSource
ds.setDataSourceName(“jdbc/xa/inventory_xa”);
// Register the DataSource implementation with a JNDI naming service,
// using the logical name “jdbc/inventory”.
ctx.bind(“jdbc/inventory”, ds);</code></pre><h3>获取连接</h3><p>调用 DataSource.getConnection 方法返回一个底层实现为 XAConnection 的逻辑 Connection 对象:</p><pre><code class="java">Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup(“jdbc/inventory”);
Connection con = ds.getConnection(“myID”,“mypasswd”);</code></pre><p>DataSource.getConnection 方法的底层实现如下所示:</p><pre><code class="java">// Assume xads is a driver’s implementation of XADataSource
XADataSource xads = (XADataSource)ctx.lookup(“jdbc/xa/" +
"inventory_xa”);
// xacon implements XAConnection
XAConnection xacon = xads.getXAConnection(“myID”, “mypasswd”);
// Get a logical connection to pass back up to the application
Connection con = xacon.getConnection();
CODE EXAMPLE 12-5 Getting a logical connection from an</code></pre><h2>XAResource</h2><p><code>XAResource</code> 接口是 JTA 规范中的一个定义,也是 Java 语言中对应 X/Open 组织的 XA 的对等概念。 <code>XAResource</code> 对象通过调用 <code>XAConnection.getXAResource</code> 方法获得,通过调用该方式将 <code>XAConnection</code> 与一个分布式事务绑定。同一时刻,一个 <code>XAConnection</code> 只能与一个分布式事务绑定。JDBC 驱动应维护 <code>XAResource</code> 与 <code>XAConnection</code> 的一对一关系,也就是说对于 getXAResource 方法的多次调用,都返回同一个 <code>XAResource</code> 对象。</p><p>通常,中间应用服务器调用 <code>XAConnection.getXAResource</code> 方法得到一个 <code>XAResource</code> 对象后,会将它交给外部的事务管理器,事务管理器并不需要直接与 <code>XAConnection</code> 交互,它直接利用 <code>XAResource</code> 对象。</p><p>事务管理器管理多个 <code>XAResource</code> 对象,每一个都代表一个参与到分布式事务的资源管理器,注意,不同的 <code>XAResource</code> 对象可能指向同一个资源管理器,当不同的事务参与者使用同一个 <code>XADataSource</code> ,就有可能出现这种情况。</p><p><code>XAResource</code> 定义了以下的方法,来完成两阶段提交协议,每个方法都要求有一个 xid 参数,用来标识一个分布式事务。</p><ul><li>start 方法。通知资源管理器后续的操作来处于一个分布式事务中。</li><li>end 方法。通知资源管理器事务结束。</li><li>prepare 方法。获取资源管理器关于事务应该回滚还是提交的投票。</li><li>commit 方法。通知资源管理器提交它的事务分支。只有当所有的参与者都投票提交全局事务时,这个方法才能被调用。</li><li>rollback 方法。通知资源管理器回滚它的事务分支。只有当至少一个事务参与者投票回滚全局事务时,这个方法才会被调用。</li></ul><p>JTA 规范中有对 XAResource 完整的描述。</p><h2>事务管理</h2><p>通过 XAResource.start 和 XAResource.end 方法来定义事务边界。边界内事务模式为全局事务。边界外的事务为本地事务。</p><p>除了一些约束,一个事务参与者的应用代码应该怎么写,与它是否参与分布式事务没有什么关系。分布式事务的边界一般都是由外部的事务管理者来定义的,事务的参与者不能调用 <code>Connection</code> 类的某些方法:</p><ul><li>setAutoCommit(true)</li><li>commit</li><li>rollback</li><li>setSavepoint</li></ul><p>如果事务参与者试图调用这些方法,JDBC 驱动应抛出 <code>SQLException</code>。当分布式事务结束后,这些方法的调用就不再有限制,并且应用于本地事务。</p><p>在事务边界内,事务参与者应该避免调用 <code>Connection.setTransactionIsolation</code> 方法,这个方法的行为不做约束,由驱动自主决定。</p><p>如果一个连接在参与分布式事务前的 autocommit 属性已经为 true,那么当它参与分布式事务时,这个属性会被忽略,当事务结束后,属性才重新生效。</p><h3>两阶段提交</h3><p>以下几个步骤阐述了事务管理器如何利用 <code>XAResource</code> 对象实现二阶段提交协议。这些步骤是基于使用具有外部事务管理器的应用服务器的“三层架构”实现的。</p><ol><li><p>应用服务器从两个不同的连接中获取 <code>XAResource</code> 对象</p><pre><code class="java">// XAConA connects to resource manager A
javax.transaction.xa.XAResource resourceA = XAConA.getXAResource();
// XAConB connects to resource manager B
javax.transaction.xa.XAResource resourceB = XAConB.getXAResource();</code></pre></li><li>应用服务器传递 <code>XAResource</code> 对象给事务管理器,事务管理器不直接与 <code>XAConnection</code> 对象交互。</li><li><p>事务管理器利用 <code>XAResource</code> 将资源管理器纳入事务中,整个事务以一个 xid 作为标识,由事务管理器在启动事务时负责生成。</p><pre><code class="java">// Send work to resource manager A. The TMNOFLAGS argument indicates
// we are starting a new branch of the transaction, not joining or
// resuming an existing branch.
resourceA.start(xid, javax.transaction.xa.TMNOFLAGS);
// do work with resource manager A
...
// tell resource manager A that it’s done, and no errors have occurred
resourceA.end(xid, javax.transaction.xa.TMSUCCESS);
// do work with resource manager B.
resourceB.start(xid, javax.transaction.xa.TMNOFLAGS);
// B’s part of the distributed transaction
...
resourceB.end(xid, javax.transaction.xa.TMSUCCESS);</code></pre></li><li><p>事务管理器启动两阶段提交协议,请求两个参与者进行投票:</p><pre><code class="java">resourceA.prepare(xid);
resourceB.prepare(xid);</code></pre></li></ol><p>如果一个事务参与者想要投票 rollback,它需要抛出一个 <code>javax.transaction.xa.XAException</code>。</p><ol start="5"><li><p>如果两个事务参与者都投票 commit,事务参与者通知两个参与者提交他们的事务分支:</p><pre><code class="java">resourceA.commit(xid, false);
resourceB.commit(xid, false);</code></pre></li><li><p>如果任何一个事务参与者投票 rollback,事务管理器则通知各个参与者回滚它们的事务分支:</p><pre><code class="java">resourceA.rollback(xid);
resourceB.rollback(xid);</code></pre></li></ol><p>事务管理器在处理一个事务分支的不同阶段的时候,不一定要用的是同一个 <code>XAResource</code> 对象,只要这两个对象的连接是来自于同一个事务管理器。</p><h2>连接关闭</h2><p>当在分布式事务环境中,应用层使用完一条连接后,中间层服务器应该得到通知。在先前对 <code>PooledConnection</code> 对象的讨论中,我们指出了当 <code>Connection.close</code> 方法被调用时,中间件服务器会作为一个 <code>ConnectionEventListener</code> 得到通知。在这时,事务管理器也会得到通知,并且结束对应的事务分支。如果 <code>DataSource</code> 包含池化模块,那么池化模块也必须得到通知,以便将 <code>XAConnection</code> 归还。</p><blockquote>注意,即使一个连接被关闭,分布式事务也依然可以处于活跃状态。</blockquote><h2>XAResource 接口的局限性</h2><p><code>XAResource</code> 接口只定义了 X/Open XA 规范中要求定义的方法。如果一个资源管理器支持了一些在 XA 规范中没有定义的特性,那么应用层无法明显地通过 API 去使用这些特性,只能交给具体的驱动在底层去处理。如果应用间接利用了这个特性,这又会带来一个可移植性的问题。</p>
阿里分布式事务中间件 Seata 用法与原理
https://segmentfault.com/a/1190000020653052
2019-10-11T17:04:16+08:00
2019-10-11T17:04:16+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>原文链接:<a href="https://link.segmentfault.com/?enc=nNEZ11BUdBHDY4fomAuvYQ%3D%3D.tnCq3K41nJ7Ci7tnNnhuhyuVCDz%2BkIcdZyRhuv8%2FHunf9ssE6rfTd9BNe8tW%2BOuT" rel="nofollow">《阿里分布式事务中间件 Seata 用法与原理》http://www.ytbean.com/posts/seata-intro/</a></p><h2>quick-start</h2><h3>案例设计</h3><p>seata 官方给出了一系列 <a href="https://link.segmentfault.com/?enc=LwNEEF9ZrWMuR5g0hfQidA%3D%3D.x8JcewAIw2GUHEm5OYao03qtQpm69IsJd9o7ajePv4h1KJ9HpAmg394W4080j%2FO%2F" rel="nofollow">demo 样例</a>,不过我在用的过程中发现总有这个那个的问题,所以自己维护了一份基于 dubbo 的 <a href="https://link.segmentfault.com/?enc=xo8IfkN0OvqU2mkMa2q5bw%3D%3D.VYpdZCKUokUswl7u%2Fhrve8SUtiTWgpXozmb5T5GpfjCXAIpXDQgdH1iA9XxcW638" rel="nofollow">demo</a> 在 github 上,适配的 seata 版本是 0.8.0。<br>案例的设计直接参考<a href="https://link.segmentfault.com/?enc=YnKPpTqDOlXdwN0LHUsBkw%3D%3D.V638Js6QMmiP1CdqK2n7uGJgVpCx%2B%2FLuIlC81Gx8gesg18%2BjjtiRZSwuN5HNbONn" rel="nofollow">官方 quick start</a>给出的案例:<br><img src="/img/remote/1460000041525908" alt="案例设计" title="案例设计"></p><p>整个案例分为三个服务,分别是存储服务、订单服务和账户服务,这些服务通过 dubbo 进行发布和调用,内部调用逻辑如上面图所示。<br>整个 demo 的工程样例如下所示:<br><img src="/img/remote/1460000041525909" alt="工程样例" title="工程样例"></p><h3>undo_log 表</h3><p>这个案例除了在数据库需要建立业务表以外,还要额外建立一张 undo_log 表,这个表的主要作用是记录事务的前置镜像和后置镜像。<br>全局事务进行到提交阶段,则删除该表对应的记录,全局事务如果需要回滚,则会利用这个表里记录的镜像数据,恢复数据。<br>undo_log 表里的数据实际上是“朝生夕死”的,数据不需要在表里存活太久。表结构如下所示:</p><pre><code>CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;</code></pre><h3>服务逻辑</h3><p>每个服务都对应了一个 starter 类,这个类主要用来在 spring 环境下,将该服务启动,并通过 dubbo 发布出去,以账户服务为例:</p><pre><code class="java">/**
* The type Dubbo account service starter.
*/
public class DubboAccountServiceStarter {
/**
* 2. Account service is ready . A buyer register an account: U100001 on my e-commerce platform
*
* @param args the input arguments
*/
public static void main(String[] args) {
ClassPathXmlApplicationContext accountContext = new ClassPathXmlApplicationContext(new String[]{"spring/dubbo-account-service.xml"});
accountContext.getBean("service");
JdbcTemplate accountJdbcTemplate = (JdbcTemplate) accountContext.getBean("jdbcTemplate");
accountJdbcTemplate.update("delete from account_tbl where user_id = 'U100001'");
accountJdbcTemplate.update("insert into account_tbl(user_id, money) values ('U100001', 999)");
new ApplicationKeeper(accountContext).keep();
}
}</code></pre><p>首先通过 <code>ClassPathXmlApplicationContext</code> 读取 dubbo-account-service.xml 这个 spring 配置文件并启动 spring 容器环境,并通过 spring 的 jdbc template 对账户表的数据进行初始化。<br>dubbo-account-service.xml 配置文件中进行了各类 bean 的配置,包括 dubbo 与 spring 结合时的标准配置:</p><pre><code class="java"> <bean id="accountDataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
<constructor-arg ref="accountDataSource" />
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="accountDataSourceProxy" />
</bean>
<dubbo:application name="dubbo-demo-account-service" />
<dubbo:registry address="zookeeper://localhost:2181" />
<dubbo:protocol name="dubbo" port="20881" />
<dubbo:service interface="io.seata.samples.dubbo.service.AccountService" ref="service" timeout="10000"/>
<bean id="service" class="io.seata.samples.dubbo.service.impl.AccountServiceImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean class="io.seata.spring.annotation.GlobalTransactionScanner">
<constructor-arg value="dubbo-demo-account-service"/>
<constructor-arg value="my_test_tx_group"/>
</bean></code></pre><p>这份配置里主要有两个需要引起注意的关键点</p><ol><li>jdbcTemplate 这个 bean 所依赖的数据源 bean,是一个类名为 io.seata.rm.datasource.DataSourceProxy 的数据源类,通过它的名字可以很明显地看出这是一个代理模式的应用,因为 seata 为完成全局事务的逻辑,需要在普通的 sql 操作前后添加一些逻辑,比如说 sql 执行前对 sql 进行语法解析,生成前置镜像,sql 执行后生成后置镜像,通过代理的方式,可以方便地对 connection,statement 等进行代理包装,在调用的时候进行拦截,加入自己的逻辑。</li><li><p>配置文件中还有一个 io.seata.spring.annotation.GlobalTransactionScanner 类型的 bean,这个 bean 是支撑 seata 能在 spring 环境中通过注解的方式来划定事务边界的基础。在 spring 容器启动时,会扫描 <code>@GlobalTransactional</code> 注解是否存在,这个注解标识了全局事务的开始和结束,也就是我们常说的“事务的边界”</p><h3>业务逻辑</h3><p>业务逻辑的具体详情在 <code>BusinessServiceImpl</code> 类中可以看到:</p><pre><code class="java"> @Override
@GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx")
public void purchase(String userId, String commodityCode, int orderCount) {
LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
storageService.deduct(commodityCode, orderCount);
orderService.create(userId, commodityCode, orderCount);
// throw new RuntimeException("xxx");
}</code></pre><p>先调用存储服务,减少库存,然后调用订单服务,新建订单。这两个动作属于一个整体的事务,任何一个动作失败,都需要撤销所有的操作。<br>这个方法也有两个需要注意的点:</p></li><li>该方法上声明了 @GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx") 这样的注解,用于让上文提到的 GlobalTransactionScanner 扫描的时候发现这是一个全局事务。</li><li>方法的最后有一行代码抛出了 RuntimeException,这主要是为了模仿全局事务的失败,并让 seata 走全局事务回滚逻辑。</li></ol><h3>事务扫描与边界定义</h3><p>上文提到的 GlobalTransactionScanner 类,会在 spring 容器启动的时候,也被初始化。<br>在它的 afterPropertiesSet 方法被调用时,会触发 seata client 的初始化</p><pre><code class="java"> @Override
public void afterPropertiesSet() {
if (disableGlobalTransaction) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Global transaction is disabled.");
}
return;
}
initClient();
}</code></pre><p>初始化客户端做的事情主要是建立与 seata server 的连接,并注册 TM 和 RM。接下来,在 wrapIfNecessary 方法里,实现对注解的扫描,并对添加了注解的方法添加 interceptor。<br>这篇文章里我们暂时不讨论 TCC 模式,只讨论 AT 模式,也暂不讨论全局事务锁 GlobalLock 的实现,先忽略这些有关的逻辑,只关注事务处理逻辑。</p><pre><code class="java"> Class<?> serviceInterface = SpringProxyUtils.findTargetClass(bean);
Class<?>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);
if (!existsAnnotation(new Class[] {serviceInterface})
&& !existsAnnotation(interfacesIfJdk)) {
return bean;
}
if (interceptor == null) {
interceptor = new GlobalTransactionalInterceptor(failureHandlerHook);
}</code></pre><p>在这里,interceptor 的实现是 GlobalTransactionalInterceptor,也就是说,以上文的案例为例子,当 BusinessServiceImpl 的 purchase 方法被调用的时候,实际上这个方法会被拦截器拦截,执行拦截器里的逻辑:</p><pre><code class="java"> @Override
public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
Class<?> targetClass = (methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null);
Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);
final GlobalTransactional globalTransactionalAnnotation = getAnnotation(method, GlobalTransactional.class);
final GlobalLock globalLockAnnotation = getAnnotation(method, GlobalLock.class);
if (globalTransactionalAnnotation != null) {
return handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation);
} else if (globalLockAnnotation != null) {
return handleGlobalLock(methodInvocation);
} else {
return methodInvocation.proceed();
}
}
private Object handleGlobalTransaction(final MethodInvocation methodInvocation,
final GlobalTransactional globalTrxAnno) throws Throwable {
try {
return transactionalTemplate.execute(new TransactionalExecutor() {
@Override
public Object execute() throws Throwable {
return methodInvocation.proceed();
}
public String name() {
String name = globalTrxAnno.name();
if (!StringUtils.isNullOrEmpty(name)) {
return name;
}
return formatMethod(methodInvocation.getMethod());
}
@Override
public TransactionInfo getTransactionInfo() {
TransactionInfo transactionInfo = new TransactionInfo();
transactionInfo.setTimeOut(globalTrxAnno.timeoutMills());
transactionInfo.setName(name());
Set<RollbackRule> rollbackRules = new LinkedHashSet<>();
for (Class<?> rbRule : globalTrxAnno.rollbackFor()) {
rollbackRules.add(new RollbackRule(rbRule));
}
for (String rbRule : globalTrxAnno.rollbackForClassName()) {
rollbackRules.add(new RollbackRule(rbRule));
}
for (Class<?> rbRule : globalTrxAnno.noRollbackFor()) {
rollbackRules.add(new NoRollbackRule(rbRule));
}
for (String rbRule : globalTrxAnno.noRollbackForClassName()) {
rollbackRules.add(new NoRollbackRule(rbRule));
}
transactionInfo.setRollbackRules(rollbackRules);
return transactionInfo;
}
});
} catch (TransactionalExecutor.ExecutionException e) {
TransactionalExecutor.Code code = e.getCode();
switch (code) {
case RollbackDone:
throw e.getOriginalException();
case BeginFailure:
failureHandler.onBeginFailure(e.getTransaction(), e.getCause());
throw e.getCause();
case CommitFailure:
failureHandler.onCommitFailure(e.getTransaction(), e.getCause());
throw e.getCause();
case RollbackFailure:
failureHandler.onRollbackFailure(e.getTransaction(), e.getCause());
throw e.getCause();
default:
throw new ShouldNeverHappenException("Unknown TransactionalExecutor.Code: " + code);
}
}
}</code></pre><p>在执行 handleGlobalTransaction 方法时,实际上采用模板模式,委托给了 TransactionalTemplate 类去执行标准的事务处理流程。如下所示:</p><pre><code class="java"> /**
* Execute object.
*
* @param business the business
* @return the object
* @throws TransactionalExecutor.ExecutionException the execution exception
*/
public Object execute(TransactionalExecutor business) throws Throwable {
// 1. get or create a transaction
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
// 1.1 get transactionInfo
TransactionInfo txInfo = business.getTransactionInfo();
if (txInfo == null) {
throw new ShouldNeverHappenException("transactionInfo does not exist");
}
try {
// 2. begin transaction
beginTransaction(txInfo, tx);
Object rs = null;
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3.the needed business exception to rollback.
completeTransactionAfterThrowing(txInfo,tx,ex);
throw ex;
}
// 4. everything is fine, commit.
commitTransaction(tx);
return rs;
} finally {
//5. clear
triggerAfterCompletion();
cleanUp();
}
}</code></pre><p>事务处理逻辑实际上是一种模板,将事务相关的处理逻辑放在 try 块里,发现异常后执行回滚,正常执行则执行提交。<br>在这里有个需要注意的地方是,seata 不把提交这个动作放在 try 块里,因为在 seata 里,全局事务的提交实际上是可以异步执行的。<br>因为全局事务如果进行到提交这一阶段,那么意味着各个分支事务已经执行过本地提交,全局事务的提交阶段仅仅是删除 undo_log 里的记录,这个记录删除或者不删除,实际上不会改变全局事务已经正常完成的事实。所以它可以用程序异步去做,或者以人工介入的方式去做,所以 seata 认为,全局事务提交失败,不需要执行回滚流程。</p><h2>设计思想</h2><h3>二阶段提交协议的由来</h3><p>X/Open 组织提出了分布式事务处理的规范 DTP 模型(Distributed Transaction Processing),该模型中主要定义了三个基本组件,分别是</p><ul><li>应用程序(Application Program ,简称AP):用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作。</li><li>资源管理器(Resource Manager,简称RM):如数据库、文件系统等,并提供访问资源的方式。</li><li>事务管理器(Transaction Manager ,简称TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。</li></ul><p>一般,我们称 TM 为事务的协调者,而称 RM 为事务的参与者。TM 与 RM 之间的通信接口,则由 XA 规范来约定。</p><p>在 DTP 模型的基础上,才引出了二阶段提交协议来处理分布式事务。</p><h3>二阶段提交基本算法</h3><h4>前提</h4><p>二阶段提交协议能够正确运转,需要具备以下前提条件:</p><ol><li>存在一个协调者,与多个参与者,且协调者与参与者之间可以进行网络通信</li><li>参与者节点采用预写式日志,日志保存在可靠的存储设备上,即使参与者损坏,不会导致日志数据的消失</li><li>参与者节点不会永久性损坏,即使后仍然可以恢复</li></ol><p>实际上,条件2和3所要求的,现今绝大多数关系型数据库都能满足。</p><h4>基本算法</h4><h5>第一阶段</h5><ol><li>协调者节点向所有参与者节点询问是否可以执行提交操作,并开始等待各参与者节点的响应。</li><li>参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。</li><li>各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。</li></ol><h5>第二阶段</h5><p>当协调者节点从所有参与者节点获得的相应消息都为"同意"时:</p><ol><li>协调者节点向所有参与者节点发出"正式提交"的请求。</li><li>参与者节点正式完成操作,并释放在整个事务期间内占用的资源。</li><li>参与者节点向协调者节点发送"完成"消息。</li><li>协调者节点收到所有参与者节点反馈的"完成"消息后,完成事务。</li></ol><p>如下图所示:<br><img src="/img/remote/1460000041525910" alt="二阶段提交基本算法" title="二阶段提交基本算法"></p><p>如果任一参与者节点在第一阶段返回的响应消息为"终止",或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:</p><ol><li>协调者节点向所有参与者节点发出"回滚操作"的请求。</li><li>参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。</li><li>参与者节点向协调者节点发送"回滚完成"消息。</li><li>协调者节点收到所有参与者节点反馈的"回滚完成"消息后,取消事务。</li></ol><p>如下图所示:<br><img src="/img/remote/1460000041525911" alt="事务提交/回滚" title="事务提交/回滚"></p><h3>缺陷分析</h3><p>二阶段提交协议除了协议本身具有的局限性之外,如果我们把以下情况也考虑在内:</p><ul><li>协调者宕机</li><li>参与者宕机</li><li>网络闪断(脑裂)</li></ul><p>那么二阶段提交协议实际上是存在很多问题的</p><h4>协议本身的缺陷</h4><p>协议本身的缺陷是指,在协议正常运行的情况下,无论全局事务最终是被提交还是被回滚,依然存在的问题,而暂不考虑参与者或者协调者宕机,或者脑裂的情况。</p><h5>性能问题</h5><p>参与者的本地事务开启后,直到它接收到协调者的 commit 或 rollback 命令后,它才会提交或回滚本地事务,并且释放由于事务的存在而锁定的资源。不幸的是,一个参与者收到协调者的 commit 或者 rollback 的前提是:协调者收到了所有参与者在一阶段的回复。<br>如果说,协调者一阶段询问多个参与者采用的是顺序询问的方式,那么一个参与者最快也要等到协调者询问完所有其它的参与者后才会被通知提交或回滚,在协调者未询问完成之前,这个参与者将保持占用相关的事务资源。<br>即使,协调者一阶段询问多个参与者采用的是并发询问的方式,那么一个参与者等待收到协调者的提交或者回滚通知的时间,将取决于在一阶段过程中,响应协调者最慢的那个参与者的响应时间。<br>无论是哪一种情况,参与者都将在整个一阶段持续的时间里,占用住相关的资源,参与者事务的处理时间增加。若此时在参与者身上有其它事务正在进行,那么其它事务有可能因为与这个延迟的事务有冲突,而被阻塞,这些被阻塞的事务,进而会引起其它事务的阻塞。<br>总而言之,整体事务的平均耗时增加了,整体事务的吞吐量也降低了。这会使得整个应用系统的延迟变高,吞吐量降低,可扩展性降低(当参与者变多的时候,延迟可能更严重)。<br>总的来说,二阶段提交协议,不是一个高效的协议,会带来性能上的损失。</p><h5>全局事务隔离性的问题</h5><p>全局事务的隔离性与单机事务的隔离性是不同的。<br>当我们在单机事务中提到不允许脏读时,那么意味着在事务未提交之前,它对数据造成的影响不应该对其它事务可见。<br>当我们在全局事务中提到不允许脏读时,意味着,在全局事务未提交之前,它对数据造成的影响不应该对其它事务可见。<br>在二阶段提交协议中,当在第二阶段所有的参与者都成功执行 commit 或者 rollback 之后,全局事务才算结束。但第二阶段存在这样的中间状态:即部分参与者已执行 commit 或者 rollback,而其它参与者还未执行 commit 或者 rollback。此刻,已经执行 commit 或者 rollback 的参与者,它对它本地数据的影响,对其它全局事务是可见的,即存在脏读的风险。对于这种情况,二阶段协议并没有任何机制来保证全局事务的隔离性,无法做到“读已提交”这样的隔离级别。</p><h4>协调者宕机</h4><p>如果在第一阶段,协调者发生了宕机,那么因为所有参与者无法再接收到协调者第二阶段的 commit 或者 rollback 命令,所以他们会阻塞下去,本地事务无法结束,<br>如果协调者在第二阶段发生了宕机,那么可能存在部分参与者接收到了 commit/rollback 命令,而部分没有,因此这部分没有接收到命令的参与者也会一直阻塞下去。<br>协调者宕机属于单点问题,可以通过另选一个协调者的方式来解决,但这只能保证后续的全局事务正常运行。而因为之前协调者宕机而造成的参与者阻塞则无法避免。如果这个新选择的协调者也宕机了,那么一样会带来阻塞的问题。</p><h4>参与者宕机</h4><p>如果在第一阶段,某个参与者发生了宕机,那么会导致协调者一直等待这个参与者的响应,进而导致其它参与者也进入阻塞状态,全局事务无法结束。<br>如果在第二阶段,协调者发起 commit 操作时,某个参与者发生了宕机,那么全局事务已经执行了 commit 的参与者的数据已经落盘,而宕机的参与者可能还没落盘,当参与者恢复过来的时候,就会产生全局数据不一致的问题。</p><h4>网络问题-脑裂</h4><p>当网络闪断发生在第一阶段时,可能会有部分参与者进入阻塞状态,全局事务无法结束。<br>当发生在第二阶段时,可能发生部分参与者执行了 commit 而部分参与者未执行 commit,从而导致全局数据不一致的问题。</p><h3>三阶段提交</h3><p><img src="/img/remote/1460000041525912" alt="三阶段提交" title="三阶段提交"></p><p>在二阶段提交中,当协调者宕机的时候,无论是在第一阶段还是在第二阶段发生宕机,参与者都会因为等待协调者的命令而进入阻塞状态,从而导致全局事务无法继续进行。因此,如果在参与者中引入超时机制,即,当指定时间过去之后,参与者自行提交或者回滚。但是,参与者应该进行提交还是回滚呢?悲观的做法是,统一都回滚。但事情往往没那么简单。</p><p>当第一阶段,协调者宕机时,那么所有被阻塞的参与者选择超时后回滚事务是最明智的做法,因为还未进入第二阶段,所以参与者都不会接收到提交或者回滚的请求,当前这个事务是无法继续进行提交的,因为参与者不知道其它参与者的执行情况,所以统一回滚,结束分布式事务。</p><p>在二阶段提交协议中的第二阶段,当协调者宕机后,由于参与者无法知道协调者在宕机前给其他参与者发了什么命令,进入了第二阶段,全局事务要么提交要么回滚,参与者如果引入超时机制,那么它应该在超时之后提交还是回滚呢,似乎怎么样都不是正确的做法。执行回滚,太保守,执行提交,太激进。</p><p>如果在二阶段提交协议中,在第一阶段和第二阶段中间再引入一个阶段,如果全局事务度过了中间这个阶段,那么在第三阶段,参与者就可以认为此刻进行提交的成功率会更大。但这难道不是治标不治本吗,当进入第三阶段,全局事务需要进行回滚时候,如果协调者宕机,那么参与者超时之后自行进行提交事务,就会造成全局事务的数据不一致。</p><p>再考虑参与者宕机的情况下,协调者应该在超时之后,对全局事务进行回滚。</p><p>总结起来,三阶段提交主要在二阶段提交的基础上,为了解决参与者和协调者宕机的问题,而引入了超时机制,并因为超时机制,附带引入中间这一层。<br>并且,三阶段提交并没有解决二阶段提交的存在的脑裂的问题。</p><h3>seata 二阶段提交</h3><p><a href="https://link.segmentfault.com/?enc=ZVD7MT9D1uNZFDuTtVwtrA%3D%3D.oW0VAMmRwiRexYUSjG%2BdOwF8wJP%2BMiqNGu4pg4Da%2FB%2BfHjFWmusknj54SJAJHr0CX6p%2BEmbBDPrY11TmrnY3pQ%3D%3D" rel="nofollow">fescar</a> 是阿里最近开源的一个关于分布式事务的处理组件,它的商业版是阿里云上的 GTS。<br>在其<a href="https://link.segmentfault.com/?enc=lTtpeK98iYK%2BnmMUWawSdQ%3D%3D.PHvwrDVxntTTY3bfpHbcRJ3nnxEqn%2FkBa%2FnZT6%2BmoXBZyYx3oZ063VZUTqm9Y4hXx6Iq7flbEsdMvC4EJcW6Cg%3D%3D" rel="nofollow">官方wiki</a>上,我们可以看到,它对XA 二阶段提交思考与改进。<br>在我们上面提到的参与者中,这个参与者往往是数据库本身,在 DTP 模型中,往往称之为 RM,即资源管理器。fescar 的二阶段提交模型,也是在 DTP 模型的基础上构建。</p><h4>RM逻辑不与数据库绑定</h4><p>fescar 2PC 与 XA 2PC 的第一个不同是,fescar 把 RM 这一层的逻辑放在了 SDK 层面,而传统的 XA 2PC,RM的逻辑其实就在数据库本身。fescar 这样做的好处是,把提交与回滚的逻辑放在了 SDK 层,从而不必要求底层的数据库必须对 XA 协议进行支持。对于业务来说,业务层也不需要为本地事务和分布式事务两类不同场景来适配两套不同的数据库驱动。<br>基于我们先前对 XA 2PC 讨论,XA 2PC 存在参与者宕机的情况,而 fescar 的 2PC 模型中,参与者实际上是 SDK。参与者宕机这个问题之所以在 XA 2PC 中是个大问题,主要是因为 XA 中,分支事务是有状态的,即它是跟会话绑定在一起的,无法跨连接跨会话对一个分支事务进行操作,因此在 XA 2PC 中参与者一旦宕机,分支事务就已经无法再进行恢复。<br>fescar 2PC 中,参与者实际上是SDK,而SDK是可以做高可用设计的。并且,在其第一阶段,分支事务实际上已经是被提交了的,后续的全局上的提交和回滚,实际上是操作数据的镜像,全局事务的提交会异步清理 undo_log,回滚则会利用保存好的数据镜像,进行恢复。fescar 的 2PC 中,实际上是利用了 TCC 规范的无状态的理念。因为全局事务的执行、提交和回滚这几个操作间不依赖于公共状态,比如说数据库连接。所以参与者实际上是可以成为无状态的集群的。<br>也就是说,在 fescar 2PC 中,协调者如果发现参与者宕机或者超时,那么它可以委托其他的参与者去做。</p><h4>第二阶段非阻塞化</h4><p><img src="/img/remote/1460000041525913" alt="fescar 与 xa" title="fescar 与 xa"></p><p>fescar 2PC 的第一阶段中,利用 SDK 中的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。<br>这就意味着在阻塞在第一阶段过后就会结束,减少了对数据库数据和资源的锁定时间,明显效率会变更高。根据 fescar 官方的说法,一个正常运行的业务,大概率是 90% 以上的事务最终应该是成功提交的,因此可以在第一阶段就将本地事务提交呢,这样 90% 以上的情况下,可以省去第二阶段持锁的时间,整体提高效率。<br>fescar 的这个设计,直接优化了 XA 2PC 协议本身的性能缺陷。</p><h4>协调者的高可用</h4><p>XA 2PC 中存在协调者宕机的情况,而 fescar 的整体组织上,是分为 server 层和 SDK 层的,server 层作为事务的协调者,fescar 的话术中称协调者为 TC(Transaction Coordinator ),称 SDK 为 TM(Transaction Manager)。截止至这篇文章发表前,fescar 的server层高可用还未实现,依据其官方的蓝图,它可能会采用集群内自行协商的方案,也可能直接借鉴高可用KV系统。自行实现集群内高可用方案,可能需要引进一套分布式一致性协议,例如raft,我认为这是最理想的方式。而直接利用高可用KV系统,例如 redis cluster,则会显得系统太臃肿,但实现成本低。</p><h4>事务的隔离性</h4><p>XA 2PC 是没有机制去支持全局事务隔离级别的,fescar 是提供全局事务的隔离性的,它把全局锁保存在了 server 层。全局事务隔离级别如果太高,性能会有很大的损耗。目前的隔离界别默认是读未提交,如果需要读已提交或者更高的级别,就会涉及到全局锁,则意味着事务的并发性会受影响。应用层业务层应该选择合适的事务隔离级别。</p><h4>脑裂的问题仍然没有完美解决</h4><p>无论是 XA 还是 fescar,都未解决上述提到的脑裂的问题。脑裂的问题主要影响了全局事务的最后的提交和回滚阶段。<br>没有完美的分布式事务解决方案,即使是 fescar 或者 GTS,它们也必然需要人工介入。但脑裂问题是小概率事件,并且不是致命性错误,可以先通过重试的方法来解决,如果不行,可以收集必要的事务信息,由运维介入,以自动或者非自动的方式,恢复全局事务。</p><h2>通信机制</h2><h3>RPC</h3><p>seata client 和 seata server 间是需要通过网络通信来传递信息的,client 发送请求消息给 server,server 根据实际的处理逻辑,可能会给 client 发送相应的响应消息,或者不响应任何消息。在 seata 中,客户端和服务端的通信实现,被抽象成来公共的模块,它的 package 位于 <code>io.seata.core.rpc</code> 中。</p><p>这个包名叫 rpc,这个包下的很多类名也有 rpc 相关的字眼,而实际上在我看来,这个通信框架并不是一个常规意义的 rpc 框架,如果硬要揪书本知识,那么 rpc 的解释如下:</p><blockquote>远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。</blockquote><p>在以 dubbo 为代表的微服务时代下,dubbo 常规意义上我们都称之为 rpc 框架,rpc 的理论原则是:程序员无需额外地为这个交互作用编程。那么对于像 dubbo 这样的 rpc 实现,它能让 client 像调用本地代码 api 一样,来调用远程 server 上的某个 method。</p><p>在 client 这一层直接面向 interface 编程,通过动态代理的方式,对上层屏蔽掉通信细节,在底层,将方法调用,通过序列化方式,封装成一个二进制数据串发送给 server,server 层解析该消息,通过反射的方式,将 interface 对应的 implemention 执行起来,将执行结果,扁平化成一个二进制数据串,回送给 client,client 收到数据后,拼装成 interface api 所定义的返回值类型的一个实例,作为方法调用的返回值。整个底层的细节,应用层面并不需要了解,应用层只需要以 interface.method 的方式,就像代码在本地执行一样,就能把远端 interface_implemention.method 给调用起来。<br><img src="/img/remote/1460000041525914" alt="rpc" title="rpc"></p><p>而 seata 的 rpc 框架上,实际上仅仅是一个普通的基于 netty 的网络通信框架,client 与 server 之间通过发送 request 和 response 来达到相互通信的目的,在 seata 中的每个 request 和 response 类,都实现了如何把自己序列化的逻辑。<br>各种消息类型,都实现了 <code>io.seata.core.protocol.MessageCodec</code> 接口</p><pre><code class="java">public interface MessageCodec {
/**
* Gets type code.
*
* @return the type code
*/
short getTypeCode();
/**
* Encode byte [ ].
*
* @return the byte [ ]
*/
byte[] encode();
/**
* Decode boolean.
*
* @param in the in
* @return the boolean
*/
boolean decode(ByteBuf in);
}</code></pre><p><img src="/img/remote/1460000041525915" alt="MessageCodec" title="MessageCodec"></p><p>以 <code>io.seata.core.protocol.GlobalBeginRequest</code> 为例,它都 decode 和 encode 实现如下所示:</p><pre><code class="java">@Override
public byte[] encode() {
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
byteBuffer.putInt(timeout);
if (this.transactionName != null) {
byte[] bs = transactionName.getBytes(UTF8);
byteBuffer.putShort((short)bs.length);
if (bs.length > 0) {
byteBuffer.put(bs);
}
} else {
byteBuffer.putShort((short)0);
}
byteBuffer.flip();
byte[] content = new byte[byteBuffer.limit()];
byteBuffer.get(content);
return content;
}
@Override
public void decode(ByteBuffer byteBuffer) {
this.timeout = byteBuffer.getInt();
short len = byteBuffer.getShort();
if (len > 0) {
byte[] bs = new byte[len];
byteBuffer.get(bs);
this.setTransactionName(new String(bs, UTF8));
}
}</code></pre><p>这意味着,发送方先对 message 做 encode 动作形成字节数组,将字节数组发往接收方,接收方收到字节数组后,对字节数组先判断 message type,再用对应的 message 类型对字节数组做 decode 动作。</p><h3>类的组织形式</h3><p>从 seata server 的入口类 <code>io.seata.server.Server</code> 分析,main 方法如下所示:</p><pre><code class="java">/**
* The entry point of application.
*
* @param args the input arguments
* @throws IOException the io exception
*/
public static void main(String[] args) throws IOException {
RpcServer rpcServer = new RpcServer(WORKING_THREADS);
int port = SERVER_DEFAULT_PORT;
//server port
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
System.err.println("Usage: sh services-server.sh $LISTEN_PORT $PATH_FOR_PERSISTENT_DATA");
System.exit(0);
}
}
rpcServer.setListenPort(port);
//log store mode : file、db
String storeMode = null;
if (args.length > 1) {
storeMode = args[1];
}
UUIDGenerator.init(1);
SessionHolder.init(storeMode);
DefaultCoordinator coordinator = new DefaultCoordinator(rpcServer);
coordinator.init();
rpcServer.setHandler(coordinator);
// register ShutdownHook
ShutdownHook.getInstance().addDisposable(coordinator);
if (args.length > 2) {
XID.setIpAddress(args[2]);
} else {
XID.setIpAddress(NetUtil.getLocalIp());
}
XID.setPort(rpcServer.getListenPort());
rpcServer.init();
System.exit(0);
}</code></pre><p>可以看到 seata server 使用一个 RpcServer 类来启动它的服务监听端口,这个端口用来接收 seata client 的消息,RpcServer 这个类是通信层的实现分析的入口。<br>在这里,SessionHolder 用来做全局事务树的管理,DefaultCoordinator 用来处理事务执行逻辑,而 RpcServer 是这两者可以正常运行的基础,这篇文章的重点在于剖析 RpcServer 的实现,进而延伸到 seata 整个通信框架的细节。<br>如果先从 RpcServer 的类继承图看的话,那么我们能发现一些与常规思维不太一样的地方,类继承图如下:</p><p><img src="/img/remote/1460000041525916" alt="继承体系" title="继承体系"></p><p>褐色部分是 netty 的类,灰色部分是 seata 的类。<br>在一般常规的思维中,依赖 netty 做一个 server,大致的思路是:</p><ol><li>定义一个 xxx server 类</li><li>在这个类中设置初始化 netty bootstrap,eventloop,以及设置相应的 ChannelHandler</li></ol><p>在这种思维下,很容易想到,server 与 ChannelHandler 之间的关系应该是一个“组合”的关系,即在我们构建 server 的过程中,应该把 ChannelHandler 当作参数传递给 server,成为 server 类的成员变量。<br>没错,这是我们一般情况下的思维。不过 seata 在这方面却不那么“常规”,从上面的类继承图中可以看到,从 RpcServer 这个类开始向上追溯,发现它其实是 ChannelDuplexHandler 的一个子类或者实例。这种逻辑让人一时很困惑,一个问题在我脑海里浮现:“当我启动一个 RpcServer 的时候,我是真的在启动一个 server 吗?看起来我好像在启动一个 ChannelHandler,可是 ChannelHandler 怎么谈得上‘启动’呢?”</p><h3>异步转同步的 Future 机制</h3><p>首先分析 AbstractRpcRemoting 这个类,它直接继承自 ChannelDuplexHandler 类,而 ChannelDuplexHandler 是 netty 中 inbound handler 和 outbound handler 的结合体。<br>AbstractRpcRemoting 的 init 方法里,仅仅通过 Java 中的定时任务执行线程池启动了一个定时执行的任务:</p><pre><code class="java">/**
* Init.
*/
public void init() {
timerExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
List<MessageFuture> timeoutMessageFutures = new ArrayList<MessageFuture>(futures.size());
for (MessageFuture future : futures.values()) {
if (future.isTimeout()) {
timeoutMessageFutures.add(future);
}
}
for (MessageFuture messageFuture : timeoutMessageFutures) {
futures.remove(messageFuture.getRequestMessage().getId());
messageFuture.setResultMessage(null);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("timeout clear future : " + messageFuture.getRequestMessage().getBody());
}
}
nowMills = System.currentTimeMillis();
}
}, TIMEOUT_CHECK_INTERNAL, TIMEOUT_CHECK_INTERNAL, TimeUnit.MILLISECONDS);
}</code></pre><p>这个定时任务的逻辑也比较简单:扫描 <code>ConcurrentHashMap<Long, MessageFuture> futures</code> 这个成员变量里的 MessageFuture,如果这个 Future 超时了,就将 Future 的结果设置为 null。逻辑虽然简单,但这个功能涉及到了异步通信里一个很常见的功能,即<strong>异步转同步</strong>的功能。</p><p>在 netty 这种基于 NIO 的通信方式中,数据的发送,接收,全部是非阻塞的,因此判断一个动作完成与否,并不能像传统的 Java 同步代码一样,代码执行完了就认为相应的动作也真正完成了,例如,在 netty 中,如果通过 channel.write(); 方法往对端发送一个数据,这个方法执行完了,并不代表数据发送出去了,</p><p>channel.write() 方法会返回一个 future,应用代码应该利用这个 future ,通过这个 future 可以知道数据到底发送出去了没有,也可以为这个 future 添加动作完成后的回调逻辑,也可以阻塞等待这个 future 所关联的动作执行完毕。</p><p>在 seata 中,存在着<strong>发送一个请求,并等待相应</strong>这样的使用场景,上层的 api 可能是这么定义的: <br><code>public Response request(Request request) {}</code><br>而基于 nio 的底层数据发送逻辑却是这样的:</p><pre><code class="bash">1. send request message
2. 为业务的请求构建一个业务层面的 future 实例
3. 阻塞等待在这个 future 上
4. 当收到对应的 response message 后,唤醒上面的 future,阻塞等待在这个 future 上的线程继续执行
5. 拿到结果,request 方法结束</code></pre><p>AbstractRpcRemoting 定义了几个数据发送相关的方法,分别是:</p><pre><code class="java">/**
* Send async request with response object.
*
* @param address the address
* @param channel the channel
* @param msg the msg
* @return the object
* @throws TimeoutException the timeout exception
*/
protected Object sendAsyncRequestWithResponse(String address, Channel channel, Object msg) throws TimeoutException;
/**
* Send async request with response object.
*
* @param address the address
* @param channel the channel
* @param msg the msg
* @param timeout the timeout
* @return the object
* @throws TimeoutException the timeout exception
*/
protected Object sendAsyncRequestWithResponse(String address, Channel channel, Object msg, long timeout) throws
TimeoutException;
/**
* Send async request without response object.
*
* @param address the address
* @param channel the channel
* @param msg the msg
* @return the object
* @throws TimeoutException the timeout exception
*/
protected Object sendAsyncRequestWithoutResponse(String address, Channel channel, Object msg) throws
TimeoutException;</code></pre><p>这几个方法就符合上面说到的<strong>发送一个请求,并等待相应</strong>这样的使用场景,上面这三个方法,其实都委托给了 <code>sendAsyncRequest</code> 来实现,这个方法的代码是这样子的:</p><pre><code class="java">private Object sendAsyncRequest(String address, Channel channel, Object msg, long timeout)
throws TimeoutException {
if (channel == null) {
LOGGER.warn("sendAsyncRequestWithResponse nothing, caused by null channel.");
return null;
}
final RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setId(RpcMessage.getNextMessageId());
rpcMessage.setAsync(false);
rpcMessage.setHeartbeat(false);
rpcMessage.setRequest(true);
rpcMessage.setBody(msg);
final MessageFuture messageFuture = new MessageFuture();
messageFuture.setRequestMessage(rpcMessage);
messageFuture.setTimeout(timeout);
futures.put(rpcMessage.getId(), messageFuture);
if (address != null) {
ConcurrentHashMap<String, BlockingQueue<RpcMessage>> map = basketMap;
BlockingQueue<RpcMessage> basket = map.get(address);
if (basket == null) {
map.putIfAbsent(address, new LinkedBlockingQueue<RpcMessage>());
basket = map.get(address);
}
basket.offer(rpcMessage);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("offer message: " + rpcMessage.getBody());
}
if (!isSending) {
synchronized (mergeLock) {
mergeLock.notifyAll();
}
}
} else {
ChannelFuture future;
channelWriteableCheck(channel, msg);
future = channel.writeAndFlush(rpcMessage);
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
MessageFuture messageFuture = futures.remove(rpcMessage.getId());
if (messageFuture != null) {
messageFuture.setResultMessage(future.cause());
}
destroyChannel(future.channel());
}
}
});
}
if (timeout > 0) {
try {
return messageFuture.get(timeout, TimeUnit.MILLISECONDS);
} catch (Exception exx) {
LOGGER.error("wait response error:" + exx.getMessage() + ",ip:" + address + ",request:" + msg);
if (exx instanceof TimeoutException) {
throw (TimeoutException)exx;
} else {
throw new RuntimeException(exx);
}
}
} else {
return null;
}
}</code></pre><p>先抛开方法的其它细节,比如说同步写还是异步写,以及发送频率控制。我们可以发现,这个方法其实从大角度来划分,就是如下的步骤:</p><ol><li>构造请求 message</li><li>为这个请求 message 构造一个 message future</li><li>发送数据</li><li>阻塞等待在 message future</li></ol><p>不过 AbstractRpcRemoting 也定义了方法用于<strong>仅发送消息,不接收响应</strong>的使用场景,如下所示:</p><pre><code>/**
* Send request.
*
* @param channel the channel
* @param msg the msg
*/
protected void sendRequest(Channel channel, Object msg) {
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setAsync(true);
rpcMessage.setHeartbeat(msg instanceof HeartbeatMessage);
rpcMessage.setRequest(true);
rpcMessage.setBody(msg);
rpcMessage.setId(RpcMessage.getNextMessageId());
if (msg instanceof MergeMessage) {
mergeMsgMap.put(rpcMessage.getId(), (MergeMessage)msg);
}
channelWriteableCheck(channel, msg);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("write message:" + rpcMessage.getBody() + ", channel:" + channel + ",active?"
+ channel.isActive() + ",writable?" + channel.isWritable() + ",isopen?" + channel.isOpen());
}
channel.writeAndFlush(rpcMessage);
}
/**
* Send response.
*
* @param msgId the msg id
* @param channel the channel
* @param msg the msg
*/
protected void sendResponse(long msgId, Channel channel, Object msg) {
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setAsync(true);
rpcMessage.setHeartbeat(msg instanceof HeartbeatMessage);
rpcMessage.setRequest(false);
rpcMessage.setBody(msg);
rpcMessage.setId(msgId);
channelWriteableCheck(channel, msg);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("send response:" + rpcMessage.getBody() + ",channel:" + channel);
}
channel.writeAndFlush(rpcMessage);
}</code></pre><p>这样的场景就不需要引入 future 机制,直接调用 netty 的 api 把数据发送出去就完事了。<br>分析思路回到有 future 的场景,发送数据后,要在 future 上进行阻塞等待,即调用 get 方法,那 get 方法什么返回呢,我们上面说到 future 被唤醒的时候,我们先不讨论 future 的实现细节,一个 future 什么时候被唤醒呢,在这种 请求-响应 的模式下,显然是收到了响应的时候。所以我们需要查看一下 AbstractRpcRemoting 的 channelRead 方法</p><pre><code class="java">@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof RpcMessage) {
final RpcMessage rpcMessage = (RpcMessage)msg;
if (rpcMessage.isRequest()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("%s msgId:%s, body:%s", this, rpcMessage.getId(), rpcMessage.getBody()));
}
try {
AbstractRpcRemoting.this.messageExecutor.execute(new Runnable() {
@Override
public void run() {
try {
dispatch(rpcMessage.getId(), ctx, rpcMessage.getBody());
} catch (Throwable th) {
LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th);
}
}
});
} catch (RejectedExecutionException e) {
LOGGER.error(FrameworkErrorCode.ThreadPoolFull.getErrCode(),
"thread pool is full, current max pool size is " + messageExecutor.getActiveCount());
if (allowDumpStack) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String pid = name.split("@")[0];
int idx = new Random().nextInt(100);
try {
Runtime.getRuntime().exec("jstack " + pid + " >d:/" + idx + ".log");
} catch (IOException exx) {
LOGGER.error(exx.getMessage());
}
allowDumpStack = false;
}
}
} else {
MessageFuture messageFuture = futures.remove(rpcMessage.getId());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String
.format("%s msgId:%s, future :%s, body:%s", this, rpcMessage.getId(), messageFuture,
rpcMessage.getBody()));
}
if (messageFuture != null) {
messageFuture.setResultMessage(rpcMessage.getBody());
} else {
try {
AbstractRpcRemoting.this.messageExecutor.execute(new Runnable() {
@Override
public void run() {
try {
dispatch(rpcMessage.getId(), ctx, rpcMessage.getBody());
} catch (Throwable th) {
LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th);
}
}
});
} catch (RejectedExecutionException e) {
LOGGER.error(FrameworkErrorCode.ThreadPoolFull.getErrCode(),
"thread pool is full, current max pool size is " + messageExecutor.getActiveCount());
}
}
}
}
}</code></pre><p>可以看到调用了 messageFuture 当 setResultMessage() 方法,设置 future 的结果,也就是说,唤醒了 future,那么阻塞在 future 的 get 方法上的线程就被唤醒了,得到结果,继续往下执行。</p><p>接下来我们讨论 MessageFuture 的实现细节,其实 seata 里面有很多种 future 相关的类,实现方式也不太一样,不过都大同小异,有的是基于 CompletableFuture 实现,有的是基于 CountDownLatch 实现。比如说,MessageFuture 就是基于 CompletableFuture 实现的,先看看它的成员变量:</p><pre><code class="java">private RpcMessage requestMessage;
private long timeout;
private long start = System.currentTimeMillis();
private transient CompletableFuture origin = new CompletableFuture();</code></pre><p>CompletableFuture 是它的一个成员变量,它被利用来阻塞当前线程。MessageFuture 的 get 方法,依赖于 CompletableFuture 的 get 方法,来实现有一定时间限制的等待,直到另一个线程唤醒 CompletableFuture。如下所示:</p><pre><code class="java">/**
* Get object.
*
* @param timeout the timeout
* @param unit the unit
* @return the object
* @throws TimeoutException the timeout exception
* @throws InterruptedException the interrupted exception
*/
public Object get(long timeout, TimeUnit unit) throws TimeoutException,
InterruptedException {
Object result = null;
try {
result = origin.get(timeout, unit);
} catch (ExecutionException e) {
throw new ShouldNeverHappenException("Should not get results in a multi-threaded environment", e);
} catch (TimeoutException e) {
throw new TimeoutException("cost " + (System.currentTimeMillis() - start) + " ms");
}
if (result instanceof RuntimeException) {
throw (RuntimeException)result;
} else if (result instanceof Throwable) {
throw new RuntimeException((Throwable)result);
}
return result;
}
/**
* Sets result message.
*
* @param obj the obj
*/
public void setResultMessage(Object obj) {
origin.complete(obj);
}</code></pre><p>既然说到了 future 机制,这里也顺便把 <code>io.seata.config.ConfigFuture</code> 提一下,它就是上面提到的基于 CountDownLatch 实现的一种 future 机制,虽然实现方式两者不一样,但完成的功能和作用是一样的。</p><pre><code class="java">private final CountDownLatch latch = new CountDownLatch(1);
/**
* Get object.
*
* @param timeout the timeout
* @param unit the unit
* @return the object
* @throws InterruptedException the interrupted exception
*/
public Object get(long timeout, TimeUnit unit) {
this.timeoutMills = unit.toMillis(timeout);
try {
boolean success = latch.await(timeout, unit);
if (!success) {
LOGGER.error(
"config operation timeout,cost:" + (System.currentTimeMillis() - start) + " ms,op:" + operation
.name()
+ ",dataId:" + dataId);
return getFailResult();
}
} catch (InterruptedException exx) {
LOGGER.error("config operate interrupted,error:" + exx.getMessage());
return getFailResult();
}
if (operation == ConfigOperation.GET) {
return result == null ? content : result;
} else {
return result == null ? Boolean.FALSE : result;
}
}
/**
* Sets result.
*
* @param result the result
*/
public void setResult(Object result) {
this.result = result;
latch.countDown();
}</code></pre><p>阻塞操作调用了 CountDownLatch 的 await 方法,而唤醒操作则调用 countDown 方法,核心在于需要把 CountDownLatch 的 latch 值设置为 1。<br>实际上,Java 语言本身已经提供了 java.util.concurrent.Future 这个类来提供 Future 机制,但 Java 原生的 Future 机制功能过于单一,比如说不能主动设置 future 的结果,也不能为它添加 listener,所有有许多像 seata 这样的软件,会选择去重新实现一种 future 机制来满足异步转同步的需求。也有像 netty 这样的软件,它不会借助类似于 countdownlatch 来实现,而是直接扩展 java.util.concurrent.Future,在它的基础上添加功能。</p><h3>防洪机制</h3><p>在 AbstractRpcRemoting 中,往外发数据的时候,它都会先进行一个检查,即检查当前的 channel 是否可写。</p><pre><code class="java">private void channelWriteableCheck(Channel channel, Object msg) {
int tryTimes = 0;
synchronized (lock) {
while (!channel.isWritable()) {
try {
tryTimes++;
if (tryTimes > NettyClientConfig.getMaxNotWriteableRetry()) {
destroyChannel(channel);
throw new FrameworkException("msg:" + ((msg == null) ? "null" : msg.toString()),
FrameworkErrorCode.ChannelIsNotWritable);
}
lock.wait(NOT_WRITEABLE_CHECK_MILLS);
} catch (InterruptedException exx) {
LOGGER.error(exx.getMessage());
}
}
}
}</code></pre><p>这要从 netty 的内部机制说起,当调用 ChannelHandlerContext 或者 Channel 的 write 方法时,netty 只是把要写的数据放入了自身的一个环形队列里面,再由后台线程真正往链路上发。如果接受方的处理速度慢,也就是说,接收的速度慢,那么根据 tcpip 协议的滑动窗口机制,它也会导致发送方发送得慢。<br>我们可以把 netty 的环形队列想像成一个水池,调用 write 方法往池子里加水,netty 通过后台线程,慢慢把池子的水流走。这就有可能出现一种情况,由于池子水流走的速度远远慢于往池子里加水的速度,这样会导致池子的总水量随着时间的推移越来越多。所以往池子里加水时应该考虑当前池子里的水量,否则最终会导致应用的内存溢出。<br>netty 对于水池提供了两个设置,一个是<strong>高水位</strong>,一个是<strong>低水位</strong>,当池子里的水高于高水位时,这个时候 channel.isWritable() 返回 false,并且直到水位慢慢降回到低水位时,这个方法才会返回 true。</p><p><img src="/img/remote/1460000041525917" alt="高低水位" title="高低水位"></p><p>上述的 channelWriteableCheck 方法,发现channel 不可写的时候,进入循环等待,等待的目的是让池子的水位下降到 low water mark,如果等待超过最大允许等待的时间,那么将会抛出异常并关闭连接。</p><h3>消息队列</h3><p>在 AbstractRpcRemoting 中,发送数据有两种方式,一种是直接调用 channel 往外写,另一种是先把数据放进“数据篮子”里,它实际上是一个 map, key 为远端地址,value为一个消息队列。数据放队列后,再由其它线程往外发。下面是 sendAsycRequest 方法的一部分代码,显示了这种机制:</p><pre><code class="java">ConcurrentHashMap<String, BlockingQueue<RpcMessage>> map = basketMap;
BlockingQueue<RpcMessage> basket = map.get(address);
if (basket == null) {
map.putIfAbsent(address, new LinkedBlockingQueue<RpcMessage>());
basket = map.get(address);
}
basket.offer(rpcMessage);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("offer message: " + rpcMessage.getBody());
}
if (!isSending) {
synchronized (mergeLock) {
mergeLock.notifyAll();
}
}</code></pre><p>但我们在 AbstractRpcRemoting 里面没有看有任何额外的线程在晴空这个 basketMap。回顾一下上面的 RpcServer 的类继承体系,接下来我们要分析一下,AbstractRpcRemotingServer 这个类。</p><p><img src="/img/remote/1460000041525918" alt="继承体系" title="继承体系"></p><p>AbstractRpcRemotingServer 这个类主要定义了于netty 启动一个 server bootstrap 相关的类,可见真正启动服务监听端口的是在这个类,先看一下它的start方法</p><pre><code class="java">@Override
public void start() {
this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupWorker)
.channel(nettyServerConfig.SERVER_CHANNEL_CLAZZ)
.option(ChannelOption.SO_BACKLOG, nettyServerConfig.getSoBackLogSize())
.option(ChannelOption.SO_REUSEADDR, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSendBufSize())
.childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketResvBufSize())
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,
new WriteBufferWaterMark(nettyServerConfig.getWriteBufferLowWaterMark(),
nettyServerConfig.getWriteBufferHighWaterMark()))
.localAddress(new InetSocketAddress(listenPort))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new IdleStateHandler(nettyServerConfig.getChannelMaxReadIdleSeconds(), 0, 0))
.addLast(new MessageCodecHandler());
if (null != channelHandlers) {
addChannelPipelineLast(ch, channelHandlers);
}
}
});
if (nettyServerConfig.isEnableServerPooledByteBufAllocator()) {
this.serverBootstrap.childOption(ChannelOption.ALLOCATOR, NettyServerConfig.DIRECT_BYTE_BUF_ALLOCATOR);
}
try {
ChannelFuture future = this.serverBootstrap.bind(listenPort).sync();
LOGGER.info("Server started ... ");
RegistryFactory.getInstance().register(new InetSocketAddress(XID.getIpAddress(), XID.getPort()));
initialized.set(true);
future.channel().closeFuture().sync();
} catch (Exception exx) {
throw new RuntimeException(exx);
}
}</code></pre><p>这个类很常规,就是遵循 netty 的使用规范,用合适的配置启动一个 server,并调用注册中心 api 把自己作为一个服务发布出去。<br>我们可以看到,配置中确实也出现了我们上文中提到过的上下水位的配置。<br>另外,channelpipeline 中,除了添加一个保持链路有效性探测的 IdleStateHandler,和一个 MessageCodec,处理事务逻辑相关的 Handler 还需要由参数传入。<br>接下来我们看 RpcServer 这个类,从它的 init 方法里,我们可以看到,它把自己做为一个 ChannelHandler,加入到了 channel pipeline 中</p><pre><code class="java">/**
* Init.
*/
@Override
public void init() {
super.init();
setChannelHandlers(RpcServer.this);
DefaultServerMessageListenerImpl defaultServerMessageListenerImpl = new DefaultServerMessageListenerImpl(
transactionMessageHandler);
defaultServerMessageListenerImpl.init();
defaultServerMessageListenerImpl.setServerMessageSender(this);
this.setServerMessageListener(defaultServerMessageListenerImpl);
super.start();
}</code></pre><p>RpcServer 自身也实现了 channelRead 方法,但它只处理心跳相关的信息和注册相关的信息,其它的业务消息,它交给父类处理,而先前我们也已经看到,父类的channelRead<br>方法里,反过来会调用 dispatch 这个抽象方法去做消息的分发,而 RpcServer 类实现了这个抽象方法,在接收到不同的消息类型是,采取不同的处理流程。<br>关于事务的处理流程的细节,本篇文章暂不涉及,后续文章再慢慢分析。</p><p>行文至此,回想我们先前提到的一个疑惑:<br>“当我启动一个 RpcServer 的时候,我是真的在启动一个 server 吗?看起来我好像在启动一个 ChannelHandler,可是 ChannelHandler 怎么谈得上‘启动’呢?”<br>是的,我们既在启动一个 server,这个 server 也实现了事务处理逻辑,它同时也是个 ChannelHandler。<br>没有一定的事实标准去衡量这样写的代码是好是坏,我们也没必要去争论 Effective Java 提到的什么时候该用组合,什么时候该用继承。</p><h2>配置机制</h2><p>seata 的客户端代码和服务端代码逻辑里,读取配置时统一采用的以下这种 API</p><pre><code>ConfigurationFactory.getInstance().getString()</code></pre><p>seata 目前(0.8.0)支持以下几种配置方式</p><ol><li>本地文件方式</li><li>zookeeper</li><li>nacos</li><li>apollo</li><li>consul</li><li>etcd</li></ol><p>在分析 seata 的配置解析细节之前,先看看 seata 对于配置解析机制的设计<br>具体来说,seata 的配置项的命名风格都是类似于 computer.apple.macbookpro 这种的文本扁平化风格。<br>它本质上还是一个结构形的配置方式,即每个具体配置项都有父节点,举个例子:</p><pre><code>computer.apple {
macbookpro = 12000
macbookair = 8000
}</code></pre><p>这种配置方式与普通的 xml 配置文件在结构上没有什么大的区别,但与 xml 相比 还是有不少的好处和优点。</p><ol><li>简洁明了,xml 本质上还是个标记型语言,引入了许多不必要的复杂标记,间接增加了解析成本</li><li>解析阶段,xml 定位到某个配置项的逻辑更加繁琐</li><li>配置更新时如果需要更新文件,xml 的文件的更新动作不够轻量级,如果依赖一些第三方实现,还会造成代码入侵,可扩展性差。</li></ol><p>相比之下,目前主流的配置中心例如 zookeeper 或者 apollo,这些软件设计之初对数据结构的选型就与 computer.apple.macbookpro 这样的配置形态很相称。<br>例如,zookeeper 的数据结构是类似于文件系统的树状风格,所以 zookeeper 之前是很适合拿来做公共配置中心的,直到后面更优秀的配置中心出现,zookeeper 才慢慢淡出配置界,毕竟 zookeeper 它更擅长做的事情是分布式一致性的协调。<br>正是 seata 采用了这种配置风格,所以 seata 在配置中心的支持这一块,就很方便地与当前主流的配置中心做集成。</p><p>试想一下,如果要把一个 xml 配置文件存到 zookeeper 上做全局管理。大概有这么两种方式吧:</p><ol><li>把整个 xml 文本存到一个 znode 上</li><li>把 xml 的结构解析称树状结构,再一个一个对应地到 zookeeper 上创建节点</li></ol><p>第一种方式,显然很 low 哦。干脆存到数据库算了。<br>第二种方式,会带来额外的解析成本,并且不容易做配置变更这样的逻辑,因为一个配置项的变更,意味着要重新在内存里生成整个 xml 文本,然后再写进本地配置文件里面。</p><p>下面说一说 seata 的配置机制,因为 seata 支持上述这么多主流的配置中心,但是实际上使用的时候,必须也只能用一个。<br>因此,一个 seata 相关的进程启动的时候,必然要从本地某个地方直到要用什么配置中心。<br>而实际上,seata 是先读取本地的一个配置文件 registry.conf,再决定要用什么样的配置方式的。<br>这个逻辑在我们上面提到的 API <code>ConfigurationFactory.getInstance()</code> 的源码里可以看到具体的细节。</p><p>下面是 buildConfiguration 方法:</p><pre><code class="java"> private static Configuration buildConfiguration() {
ConfigType configType = null;
String configTypeName = null;
try {
// 这里读取的是本地 registry.conf 配置文件,获取配置中心的类型
configTypeName = CURRENT_FILE_INSTANCE.getConfig(
ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
+ ConfigurationKeys.FILE_ROOT_TYPE);
configType = ConfigType.getType(configTypeName);
} catch (Exception e) {
throw new NotSupportYetException("not support register type: " + configTypeName, e);
}
if (ConfigType.File == configType) {
String pathDataId = ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
+ FILE_TYPE + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
+ NAME_KEY;
String name = CURRENT_FILE_INSTANCE.getConfig(pathDataId);
return new FileConfiguration(name);
} else {
return EnhancedServiceLoader.load(ConfigurationProvider.class, Objects.requireNonNull(configType).name())
.provide();
}
}</code></pre><p>可以看到,seata 先读取本地的一个配置文件,默认是读取 registry.conf 文件,再从这个文件里面读取具体的配置类型。<br>那么 registry.conf 里面关于配置中心的配置是怎么样的呢,大概像这样:</p><pre><code class="java">config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}</code></pre><p><code>buildConfiguration()</code> 方法会先读取 type,再根据 type 继续读取这个 type 所对应的具体配置。<br>比如说,type 如果是 file 类型,那么就会读取 config.file.name,获取到本地配置文件的名字,然后再去读取具体的配置文件。<br>如果 type 是 zk,那么就会读取 config.zk.* ,获取到 zk 的地址信息,然后通过请求 zk 的方式获取存放在 zk 上的信息。</p><p>读取配置时,主要是通过 <code>Configuration</code> 接口去获取,<code>Configuration</code> 接口有许多不同的实现,例如 <code>FileConfiguration</code> 或者 <code>ZooKeeperConfiguration</code>,分别代表不同的配置中心。</p><p>通过 <code>buildConfiguration()</code> 代码的逻辑可以看出,除了本地文件配置方式,其它配置方式都是采用 SPI 的服务发现机制进行扩展的。<br>不过这个 SPI 并不是直接用的 JDK 本身自带的 SPI 机制,而是 seata 自己实现的一种比 JDK SPI 功能更多的 SPI 机制,不过两者的主要思想是一样的。<br>阿里系的很多中间件软件都很喜欢用 SPI 机制,比如说 Dubbo,也是疯狂地 SPI。<br>这种自己实现的 SPI 机制的好处在于灵活性比较强,比如说可以自定义注解来标识实现类的优先级。</p><p><code>Configuration</code> 接口定义的方法基本上是很基本的 getInt 或者 getString 这类方法,例如:</p><pre><code class="java"> /**
* Gets int.
*
* @param dataId the data id
* @param defaultValue the default value
* @param timeoutMills the timeout mills
* @return the int
*/
int getInt(String dataId, int defaultValue, long timeoutMills);
/**
* Gets int.
*
* @param dataId the data id
* @param defaultValue the default value
* @return the int
*/
int getInt(String dataId, int defaultValue);</code></pre><p>这里有个需要注意的地方是,这些方法都有两个不同的参数列表,多了一个 timeoutMillis。<br>因为读取配置已经不能假设在本地文件读取来,而是有可能通过网络去某个注册中心读取,因为需要经过网络,那么必然会有读取不到的情况,这个参数是用来限制配置读取的超时时间。</p><p>接下来以 <code>FileConfiguration</code> 这个实现来看看 seata 的一些配置解析细节。<br>首先,本地配置文件的读取, seata 引用的是 typesafe 公司的一个解析库叫做 Config,这个解析库支持的配置风格,就是包括上文展示的 registry.conf 的这种类似于 json 的风格。<br>其官方介绍是这样的:纯Java写成、零外部依赖、代码精简、功能灵活、API友好。支持Java properties、JSON、JSON超集格式HOCON以及环境变量。<br>不过,它有一个重要的功能未实现,那就是配置文件的写入,所以目前 seata 也没有配置变更后更新本地配置文件的功能。<br>可以看一下 <code>ConfigOperateRunnable</code> 的 run 方法:</p><pre><code class="java"> @Override
public void run() {
if (null != configFuture) {
if (configFuture.isTimeout()) {
setFailResult(configFuture);
return;
}
try {
if (configFuture.getOperation() == ConfigOperation.GET) {
String result = fileConfig.getString(configFuture.getDataId());
configFuture.setResult(result);
} else if (configFuture.getOperation() == ConfigOperation.PUT) {
//todo
configFuture.setResult(Boolean.TRUE);
} else if (configFuture.getOperation() == ConfigOperation.PUTIFABSENT) {
//todo
configFuture.setResult(Boolean.TRUE);
} else if (configFuture.getOperation() == ConfigOperation.REMOVE) {
//todo
configFuture.setResult(Boolean.TRUE);
}
} catch (Exception e) {
setFailResult(configFuture);
LOGGER.warn("Could not found property {}, try to use default value instead.",
configFuture.getDataId());
}
}
}</code></pre><p>赫然写着 TODO。</p><p>前面提到来读取配置有超时,那 seata 读取的时候怎么样做到检测超时呢,从下面这个方法入手:</p><pre><code class="java"> @Override
public String getConfig(String dataId, String defaultValue, long timeoutMills) {
String value;
if ((value = getConfigFromSysPro(dataId)) != null) {
return value;
}
ConfigFuture configFuture = new ConfigFuture(dataId, defaultValue, ConfigOperation.GET, timeoutMills);
configOperateExecutor.submit(new ConfigOperateRunnable(configFuture));
return (String)configFuture.get();
}</code></pre><p>每次读取配置时,都会读取封装成一个任务,扔给一个指定的线程池,再通过 ConfigFuture 去获取结果,ConfigFuture 是支持超时时间的设置的,只不过这里 FileConfiguration 没有在乎这个超时时间,毕竟超时是用来针对需要通过网络访问的第三方配置中心的。<br>这里用到了一个“异步转同步”的思想,也比较好地运用了 Future 机制。</p><p>此外,Configuration 接口还定义了配置变更监听器相关的接口,允许监听某个配置项的变更。不过这块实现还不是很完整,比如说 zookeeper 的监听器目前还没有任何一种实现。</p><h2>SDK 执行逻辑分析</h2><p>seata 客户端在处理事务逻辑的时候,实际上采用模板模式,委托给了 TransactionalTemplate 类去执行标准的事务处理流程,如下代码所示:</p><pre><code class="java"> public Object execute(TransactionalExecutor business) throws Throwable {
// 1. get or create a transaction
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
// 1.1 get transactionInfo
TransactionInfo txInfo = business.getTransactionInfo();
if (txInfo == null) {
throw new ShouldNeverHappenException("transactionInfo does not exist");
}
try {
// 2. begin transaction
beginTransaction(txInfo, tx);
Object rs = null;
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3.the needed business exception to rollback.
completeTransactionAfterThrowing(txInfo,tx,ex);
throw ex;
}
// 4. everything is fine, commit.
commitTransaction(tx);
return rs;
} finally {
//5. clear
triggerAfterCompletion();
cleanUp();
}
}</code></pre><p>在客户端的事务处理流程中,流程比较清晰,处理流程也不复杂,除了客户端自身采用了一些机制,其实 seata 把比较“重”的逻辑都放在了 server 端。<br>比如说,开启一个全局事务时,事务 id 如何生成,事务的信息如何存储,这些是不需要客户端的关心的。</p><p>当我们使用标准的 JDBC 规范来处理单库数据库事务时,代码几乎都和下面是同一个模板:</p><pre><code>conn.setAutocommit(false);
try {
// 在这里使用 connection 进行 sql 操作;
conn.xxxxxx
//如果一切正常,则直接进行提交
conn.commit();
} catch (Exception e) {
conn.rollback();
}</code></pre><p>虽然分布式事务和单机事务在“分布式” 和 “单机” 上区别很大,但在 “事务” 这个角度,却是相同的。<br>我们可以看到 seata 客户端的事务处理逻辑,跟单机事务的处理逻辑大同小异。<br>有差异的两个地方主要是:</p><ol><li>seata 中的全局事务如果提交失败,是不需要进行回滚,会有别的补救措施。</li><li>针对事务主体执行期间发生的异常,是不一定要回滚的,遇到有些异常可以直接提交,而有时候又可以直接忽略,不过这块跟具体场景有关系,默认出现了异常就是要回滚的。</li></ol><h3>事务信息</h3><p>TransactionTemplate 处理事务时,必须知道事务的配置信息,包括:</p><ol><li>超时时间</li><li>事务标识名称</li><li>回滚时机或者不回滚时机</li></ol><p>这些信息主要通过在方法上的 @GlobalTransactional 注解携带进来,我们可以看一下这个注解包含了哪些属性:</p><pre><code class="java">@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface GlobalTransactional {
int timeoutMills() default TransactionInfo.DEFAULT_TIME_OUT;
String name() default "";
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}</code></pre><p>这些信息由 TransactionExecutor 携带给 TransactionTemplate。<br>TransactionExecutor 这个类除了携带事务信息 TransactionInfo,它还携带了事务的执行主体,即标识了 @GlobalTransactional 注解的方法的方法体。<br>调用 TransactionExecutor 的 invoke 方法,就相当于执行了事务的执行主体。<br>TransactionExecutor 是一个接口,我们通过观察它的匿名实现类的构造方式,就能正式上面说的观点,例如 GlobalTransactionInterceptor 的 handleGlobalTransaction 方法:</p><pre><code class="java"> private Object handleGlobalTransaction(final MethodInvocation methodInvocation,
final GlobalTransactional globalTrxAnno) throws Throwable {
try {
return transactionalTemplate.execute(new TransactionalExecutor() {
@Override
public Object execute() throws Throwable {
return methodInvocation.proceed();
}
public String name() {
String name = globalTrxAnno.name();
if (!StringUtils.isNullOrEmpty(name)) {
return name;
}
return formatMethod(methodInvocation.getMethod());
}
@Override
public TransactionInfo getTransactionInfo() {
TransactionInfo transactionInfo = new TransactionInfo();
transactionInfo.setTimeOut(globalTrxAnno.timeoutMills());
transactionInfo.setName(name());
Set<RollbackRule> rollbackRules = new LinkedHashSet<>();
for (Class<?> rbRule : globalTrxAnno.rollbackFor()) {
rollbackRules.add(new RollbackRule(rbRule));
}
for (String rbRule : globalTrxAnno.rollbackForClassName()) {
rollbackRules.add(new RollbackRule(rbRule));
}
for (Class<?> rbRule : globalTrxAnno.noRollbackFor()) {
rollbackRules.add(new NoRollbackRule(rbRule));
}
for (String rbRule : globalTrxAnno.noRollbackForClassName()) {
rollbackRules.add(new NoRollbackRule(rbRule));
}
transactionInfo.setRollbackRules(rollbackRules);
return transactionInfo;
}
});
} catch (TransactionalExecutor.ExecutionException e) {
TransactionalExecutor.Code code = e.getCode();
switch (code) {
case RollbackDone:
throw e.getOriginalException();
case BeginFailure:
failureHandler.onBeginFailure(e.getTransaction(), e.getCause());
throw e.getCause();
case CommitFailure:
failureHandler.onCommitFailure(e.getTransaction(), e.getCause());
throw e.getCause();
case RollbackFailure:
failureHandler.onRollbackFailure(e.getTransaction(), e.getCause());
throw e.getCause();
default:
throw new ShouldNeverHappenException("Unknown TransactionalExecutor.Code: " + code);
}
}</code></pre><h3>全局事务的创建</h3><ul><li>事务的发起者,需要创建全局事务,向 seata server 申请全局事务 id</li><li>事务的参与者,则需要获取已经创建好的全局事务,以便将自己注册为正确全局事务下的一个事务分支,认清自己的身份....</li></ul><p>GlobalTransactionContext 这个类负责上述说的两个功能,事务发起者调用它提供的 getCurrentOrCreate 方法创建全局事务,事务的参与者调用它的 getCurrent 方法获取当前所处的全局事务(实际上参与者也是根据全局事务 xid 进行创建)。<br>通过这些方法获取到的是一个 GlobalTransaction 接口的具体实现实例。代表了一个全局事务。<br>GlobalTransaction 提供了事务的操作流程 api,例如基本的 begin、commit 和 rollback,另外还有事务的状态,还有 server 分配的全局事务 id;<br>接口定义如下所示:</p><pre><code class="java">public interface GlobalTransaction {
void begin() throws TransactionException;
void begin(int timeout) throws TransactionException;
void begin(int timeout, String name) throws TransactionException;
void commit() throws TransactionException;
void rollback() throws TransactionException;
GlobalStatus getStatus() throws TransactionException;
String getXid();
}</code></pre><p>无论是参与者还是发起者,都需要在创建 GlobalTransaction 实例的时候,把它自己的身份讲清楚。由 GlobalTransactionRole 来定义这些角色。</p><h3>全局事务 xid 的传播</h3><p>全局事务的参与者和发起者是不要求部署在同一个操作系统环境下的,否则就分布式事务就谈不上分布式了。<br>因此,全局事务的发起者在向 server 注册了全局事务 id 后,必须通过某种方式把全局事务 xid 通过服务的调用链传下去。<br>如果 seata 是在 dubbo 的环境下运行的,那么通过 dubbo 调用方式(例如说泛化调用、或者可变参数、或者直接修改底层协议),就能够在服务调用时,将 xid 在整个服务调用链上传播。<br>在 seata 中,RootContext 这个类,就是用来保存参与者和发起者之间传播的 xid 的。传播流程大致如下:</p><ol><li>发起者开启全局事务后,将 xid 塞进 RootContext 里</li><li>服务框架将 xid 沿着服务调用链一直传播,塞进每个参与者进程的 RootContext 里。</li><li>参与者发现 RootContext 里的 xid 存在时,它便知道自己处于全局事务中,并且知道 xid 是什么。</li></ol><p>通过 GlobalTransactionContext 的 getCurrent 方法,我们可以看到这些事实:</p><pre><code class="java"> /**
* Get GlobalTransaction instance bind on current thread.
*
* @return null if no transaction context there.
*/
private static GlobalTransaction getCurrent() {
String xid = RootContext.getXID();
if (xid == null) {
return null;
}
return new DefaultGlobalTransaction(xid, GlobalStatus.Begin, GlobalTransactionRole.Participant);
}</code></pre><h3>与 server 端如何交互</h3><p>GlobalTransaction 定义的事务操作 api,其具体实现类需要真正与服务端通信了。以 begin 这个 api 为例,它的默认实现类 DefaultGlobalTransaction 是这样实现的:</p><pre><code class="java"> public void begin(int timeout, String name) throws TransactionException {
if (role != GlobalTransactionRole.Launcher) {
check();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Ignore Begin(): just involved in global transaction [" + xid + "]");
}
return;
}
if (xid != null) {
throw new IllegalStateException();
}
if (RootContext.getXID() != null) {
throw new IllegalStateException();
}
xid = transactionManager.begin(null, null, name, timeout);
status = GlobalStatus.Begin;
RootContext.bind(xid);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Begin new global transaction [" + xid + "]");
}
}</code></pre><p>实际上除了一些必要的条件检查和其它附带操作,它把与服务端具体的通信委托给了 TransactionManager 去做,那我们再看看 TransactionManager 做了什么事:</p><pre><code class="java"> @Override
public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
throws TransactionException {
GlobalBeginRequest request = new GlobalBeginRequest();
request.setTransactionName(name);
request.setTimeout(timeout);
GlobalBeginResponse response = (GlobalBeginResponse)syncCall(request);
if (response.getResultCode() == ResultCode.Failed) {
throw new TransactionException(TransactionExceptionCode.BeginFailed, response.getMsg());
}
return response.getXid();
}</code></pre><p>逻辑也十分简单,就是发送一个请求,然后接收一个响应。<br>不得不说,seata 在代码逻辑的抽象和结构层次这方面做得很好。</p><h3>钩子及异常处理</h3><p>seata 客户端通过 TransactionHook 这个接口,在一个事务的处理过程中,允许为它的各个阶段添加钩子逻辑</p><pre><code class="java">public interface TransactionHook {
void beforeBegin();
void afterBegin();
void beforeCommit();
void afterCommit();
void beforeRollback();
void afterRollback();
void afterCompletion();
}</code></pre><p>由于全局事务的提交和回滚依然是由可能失败的,比如说全局事务如果提交失败,意味着 undo_log 表里的数据目前是没有删除成功的,可能需要记录并在后面重试删除;<br>如果回滚失败,意味着当前这个全局事务属于异常结束,需要特殊处理、甚至人工介入。<br>seata 通过 FailureHandler 这个接口,提供了一个可扩展的点:</p><pre><code class="java">public interface FailureHandler {
void onBeginFailure(GlobalTransaction tx, Throwable cause);
void onCommitFailure(GlobalTransaction tx, Throwable cause);
void onRollbackFailure(GlobalTransaction tx, Throwable cause);
}</code></pre><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
JDBC 4.2 Specifications 中文翻译 -- 第十一章 连接池
https://segmentfault.com/a/1190000017130686
2018-11-24T20:19:48+08:00
2018-11-24T20:19:48+08:00
ytbean
https://segmentfault.com/u/ytbean
1
<p>在基本的 <code>DataSource</code> 实现中,客户端的 Connection 对象与物理数据库连接有着1:1的关系。当 Connection 被关闭以后,物理连接也会被关闭。因此,连接的频繁打开、初始化以及关闭,会在一个客户端会话中上演多次,带来了过重的性能消耗。<br>而连接池就能解决这个问题,连接池维护了一系列物理数据库连接的缓存,可以被多个客户端会话重复使用。连接池能够极大地提高性能和可扩展性,特别是在一个三层架构的环境中,大量的客户端可以共享一个数量比较小的物理数据库连接池。在图11-1中,JDBC 驱动提供了一个 ConnectionPoolDataSource 的实现,应用服务器可以用它来创建和管理连接池。<br><img src="/img/remote/1460000017130689" alt="ConnectionPoolDataSource实现.png" title="ConnectionPoolDataSource实现.png"></p>
<p>连接池的管理策略跟具体的实现有关,也跟具体的应用服务器有关。应用服务器对客户端提供了一个 DataSource 接口的具体实现,使得连接池化对于客户端来说是透明的。最终,客户端使用 DataSource API 就能和之前使用 JNDI 一样,获得了更好的性能和可扩展性。</p>
<p>下文将会介绍 ConnectionPoolDataSource 接口、PooledConnection 接口以及 ConnectionEvent 类,这三个组成部分是一个相互合作的关系,下文将以一个经典线程池的实现的角度,逐步描述这几部分。这一章也会介绍基本的 DataSource 对象和池化的 DataSource 对象之间的区别,此外,还会讨论一个池化的连接如何能够维护一堆可重用的 PreparedStatement 对象。</p>
<p>尽管本章中的所有讨论都是假设在三层架构环境下的,但连接的池化在两层架构的环境下也同样有用。<br>在两层架构的环境中,JDBC 驱动既实现了 DataSource 接口,也实现 ConnectionPoolDataSource 接口,这种实现方式允许客户端打开或者关闭多个连接。</p>
<h2>11.1 ConnectionPoolDataSource 和 PooledConnection</h2>
<p>一般来说, 一个 JDBC 驱动会去实现 ConnectionPoolDataSource 接口,应用服务器可以使用这个接口来获得 PooledConnection 对象,以下代码展示了 getPooledConnection 方法的两种版本</p>
<pre><code>public interface ConnectionPoolDataSource {
PooledConnection getPooledConnection() throws SQLException;
PooledConnection getPooledConnection(String user, String password) throws SQLException;
}</code></pre>
<p>一个 PooledConnection 对象代表一条与数据源之间的物理连接。JDBC 驱动对于 PooledConnection 的实现,则会封装所有与维护这条连接相关的细节。<br>应用服务器则会在它的 DataSource 接口的实现中,缓存和重用这些 PooledConnection。当客户端调用 DataSource.getConnection 方法时,应用服务器将会使用物理 PooledConnection 去获取一个逻辑 Connection 对象。以下代码是 PooledConnection 接口的一些方法定义:</p>
<pre><code>public interface PooledConnection {
Connection getConnection() throws SQLException;
void close() throws SQLException;
void addConnectionEventListener(
ConnectionEventListener listener);
void addStatementEventListener(
StatementEventListener listener);
void removeConnectionEventListener(
ConnectionEventListener listener);
void removeStatementEventListener(
StatementEventListener listener);
}</code></pre>
<p>当客户端使用完连接以后,它使用 Connection.close 方法来关闭这条逻辑连接,这个动作只是关闭了逻辑连接,但并不会关闭物理连接。物理连接会被归还到池子里,以待重用。<br>在这里,连接的池化对于客户端来说完全是透明的,客户端能像使用非池化连接那样去使用池化连接。</p>
<blockquote>需要注意的是,当对池化的连接调用 Connection.close() 方法时,之前通过 Connection.setClientInfo 设置的属性将会被清除掉。</blockquote>
<h2>11.2 连接事件</h2>
<p>回忆先前说过的,当 Connection.close 方法被调用后,底层的物理连接 PooledConnection 就可以再次被重用。当一个 PooledConnection 可以被回收的时候,将会使用 JavaBean 风格的事件去通知连接池管理器(应用服务器)。<br>为了发生连接事件时能被通知到,连接池管理器必须实现 ConnectionEventListener 接口,然后 PooledConnection 会将其注册为连接事件的一个监听者。ConnectionEventListener 接口定义了两个方法,也体现出了可能发生的两种不同的事件:</p>
<ul>
<li>connectionClosed 事件 --- 当逻辑连接 Connection.close 被调用时产生此事件</li>
<li>connectionErrorOccurred --- 当出现一些致命的错误,比如说数据库宕机导致连接丢失的时候,会触发这个事件</li>
</ul>
<p>连接池管理器通过调用 PooledConnection.addConnectionEventListener 方法来将自己注册为一个 PooledConnection 的监听者。一般情况下,注册的动作都发生在将连接归还到池子里之前。<br>JDBC 驱动负责在对应的事件发生的时候,调用回调方法,这两个方法都需要一个 ConnectionEvent 对象作为参数,通过这个对象可以判断到底是哪个 PooledConnection 被关闭了或者发生了错误。<br>当客户端关闭了逻辑连接的时候,JDBC 驱动会通过调用监听者所实现的 connectionClosed 方法来通知监听者,此时,监听者(连接池管理器)可以将该连接归还到池子里以便重用。当致命性错误发生时,JDBC 驱动首先会调用监听者实现的 connectionErrorOccurred 方法,然后再抛出一个 SQLException 异常。这个时候,监听者就可以通过 PooledConnection.close 方法来将物理连接关闭。</p>
<h2>11.3 三层架构环境中的连接池化</h2>
<p>以下步骤列出了客户端使用连接池池化时,实际上发生的事情:</p>
<ul>
<li>客户端调用 DataSource.getConnection 方法</li>
<li>应用服务器在它自己支持连接池的 DataSource 实现中,查找是否有可用的 PooledConnection 对象</li>
<li>如果没有可用的 PooledConnection 对象,应用服务器调用 ConnectionPoolDataSource.getPooledConnection 来创建一条物理连接,JDBC 驱动的具体实现会负责连接创建的具体细节,并把它交给应用服务器管理。</li>
<li>无论是新建的 PooledConnection 还是已经创建好的处于可用状态的,应用服务器会对这条连接进行一些标识,标记它处于正在使用的状态。</li>
<li>应用服务器调用 PooledConnection.getConnection 方法来获得一个逻辑上的 Connection 对象,这个对象底层实际上关联了一个物理的 PooledConnection 对象,客户端调用 DataSource.getConnection 方法,返回值拿到的是逻辑上的 Connection 对象。</li>
<li>应用服务器通过调用 PooledConnection.addConnectionEventListener 方法将它自己注册为一个 ConnectionEventListener,当 PooledConnection 处于可用状态时,应用服务器就会得到相应的事件通知。</li>
<li>由 DataSource.getConnection 方法返回的逻辑 Connection 对象,依然是使用 Connection API,直到 Connection.close 被调用之前,底层的 PooledConnection 都处于使用状态,不可被重用。</li>
</ul>
<p>即使在没有应用服务器的两层架构环境中,连接依然可以做到池化。这种情况下,JDBC 驱动需要实现 DataSource 接口和 ConnectionPoolDataSource 接口。</p>
<h2>11.4 DataSource 实现与连接池化</h2>
<p>抛开对性能和扩展性的提升不说,客户端使用 DataSource 接口的时候,不需要去关心它底层的实现是否池化,客户端面向的是一套统一的,无差别的使用方式。<br>常规的 DataSource 实现,即不实现连接池化功能的实现,一般由 JDBC 驱动实现,通常有以下两个观点被认为是正确的:</p>
<ul>
<li>DataSource.getConnection 方法创建一个新的 Connection 对象来代表一条真正的物理连接,并且封装了所有维护和管理这条物理连接的细节。</li>
<li>Connection.close 方法关闭底层的物理连接并释放相关的资源</li>
</ul>
<p>在一个实现了池化的 DataSource 实现中,情况则有些不一样,以下几个观点被认为是正确的:</p>
<ul>
<li>在 DataSource 的实现中,包含了一个提供了连接池化功能的模块,这个模块要怎么实现没有一个统一的标准,因人而异。这个模块会缓存一系列 PooledConnection 对象。DataSource 的实现类,通常处于驱动实现的 ConnectionPoolDataSource 和 PooledConnection 接口的上层。</li>
<li>DataSource.getConnection 方法会调用 PooledConnection 方法去获得对底层物理连接的一个句柄,如果已有的连接池里没有现成可用的连接,那么这个时候就需要新建物理连接,只有在这种情况下,新建物理连接对性能的消耗才体现出来。当需要创建新的物理连接的时候,ConnectionPoolDataSource 的 getPooledConnection 会被调用,对于物理连接的管理细节,则委托给了 PooledConnection 对象。</li>
<li>Connection.close 方法被调用时,只是关闭逻辑上的连接句柄,并不会关闭实际上的物理连接。连接池管理者此时会收到一个事件通知,被告知一个 PooledConnection 处于可重用状态了。如果此时客户端仍然企图使用这个逻辑上的连接句柄,那么只会得到一个 SQLException 异常。</li>
<li>一个物理 PooledConnection 在它的整个生命周期中,可能会产生许多的逻辑 Connection 对象,但只有最近一次产生的 Connection 对象才是有效的,当 PooledConnection.getConnection 方法被调用时,先前已经存在的 Connection 对象,将会被自动关闭。这种情况下,关闭不会产生相应的事件去通知监听者。</li>
</ul>
<blockquote>这给了应用服务器一种从客户端强行拿走连接的方式,这种情形可能很少见,但是当应用服务器需要进行强制关闭时,这个特性可能会很有用</blockquote>
<ul><li>连接池的管理者通过调用 PooledConnection.close 方法来关闭物理连接,一般发生以下情况时,才会这么做:当应用服务器正常退出时,当需要重新初始化连接的缓存时,或者是该连接上发生一些不可恢复的致命性错误时。</li></ul>
<h2>11.5 部署</h2>
<p>进行连接池化的部署,需要提供一个客户端代码可以接触到的 DataSource 对象,并且还需要把一个 ConnectionPoolDataSource 对象注册到 JNDI 中。<br>第一步,部署 ConnectionPoolDataSource,如下代码所示:</p>
<pre><code>// ConnectionPoolDS implements the ConnectionPoolDataSource
// interface. Create an instance and set properties.
com.acme.jdbc.ConnectionPoolDS cpds = new com.acme.jdbc.ConnectionPoolDS();
cpds.setServerName(“bookserver”);
cpds.setDatabaseName(“booklist”);
cpds.setPortNumber(9040);
cpds.setDescription(“Connection pooling for bookserver”);
// Register the ConnectionPoolDS with JNDI, using the logical name
// “jdbc/pool/bookserver_pool”
Context ctx = new InitialContext();
ctx.bind(“jdbc/pool/bookserver_pool”, cpds); </code></pre>
<p>上述步骤做好以后,ConnectionPoolDataSource 对象就可以被对客户端代码可见的 DataSource 使用了,DataSource 的部署需要依赖于先前部署的 ConnectionPoolDataSource,如下代码所示:</p>
<pre><code>// PooledDataSource implements the DataSource interface.
// Create an instance and set properties.
com.acme.appserver.PooledDataSource ds =
new com.acme.appserver.PooledDataSource();
ds.setDescription(“Datasource with connection pooling”);
// Reference the previously registered ConnectionPoolDataSource
ds.setDataSourceName(“jdbc/pool/bookserver_pool”);
// Register the DataSource implementation with JNDI, using the logical
// name “jdbc/bookserver”.
Context ctx = new InitialContext();
ctx.bind(“jdbc/bookserver”, ds);</code></pre>
<p>到此,客户端代码就可以使用这个 DataSource 了。</p>
<h2>11.6 池化连接的 Statement 重用</h2>
<p>JDBC 规范对于 statement 的池化也提供了一些支持。statement 池化这个特性,能让应用层像 connection 重用一样,对 PreparedStatement 进行重用,这个特性需要以连接池化为基础。<br>下图展示了 PooledConnection 与 PreparedStament 之间的关系。逻辑 Connection 可以透明地使用多个 PreparedStatement 对象。</p>
<p><img src="/img/remote/1460000017130690" alt="statement池化.png" title="statement池化.png"></p>
<p>上图中,连接池和 statement 池由应用服务器来实现。不过,这些功能其实也可以由驱动来实现,或者是数据源来实现。这里我们对于 statement 池化的讨论,其实是适用于以上提到的所有实现方式的。</p>
<h3>11.6.1 使用池化 Statement</h3>
<p>对于 statement 的重用,必须对应用透明。也就是说,从应用开发的角度,对一个 statement 的使用,不需要关心它是否是池化的实现。statement 在底层会一直保持处于打开状态,应用层的代码也不需要改变。如果应用层关闭了这个 statement,它依然需要调用 Connection.prepareStatement 方法来继续使用它。statement 的池化对于应用层来说,使用方式上是透明的,应用层唯一能感知到不同的,是它带来的明显的性能提升。<br>应用层需要通过调用 DatabaseMetadata 的 supportStatementPooling 方法,来判断一个数据源是否支持 statement 重用。<br>在很多情况下,对于 statement 的重用,是一种非常有意义的优化,尤其是负责的 prepared statement。不过,需要注意的是,大量的 statement 处于打开状态,有可能会对资源带来影响。</p>
<h3>11.6.2 关闭池化 Statement</h3>
<p>一旦应用层关闭了一个 statement,无论它是否是池化的,它都不能再继续被使用了,否则会导致异常抛出。<br>以下几个方法会关闭一个池化的 statement:</p>
<ul>
<li>Statement.close --- 由应用层调用。如果一个 statement 是池化的,调用这个方法会关闭逻辑上的 statement,但不会关闭底层的已经池化的物理 statement。</li>
<li>
<p>Connection.close --- 由应用层调用。</p>
<ul>
<li>非池化连接 --- 关闭底层的物理连接和由这个连接创建的所有 statement。这样做是必要的,因为垃圾回收机制无法检测到外部的资源什么时候会被释放。</li>
<li>池化连接 --- 仅关闭逻辑上的连接和这个连接所创建的逻辑 statement,底层的物理连接以及相关的 statement 不会被关闭。</li>
</ul>
</li>
<li>PooledConnection.close --- 由连接池管理者调用。会关闭底层的物理连接以及所有相关的 statement。</li>
</ul>
<p>应用层无法直接关闭一个已经池化的物理 statement,这是连接池管理器做的事情。PooledConnection.close 方法关闭物理连接以及所有的关联 statement,释放掉相关的资源。<br>应用层也无法直接控制 statement 应该如何被池化。一个池化的 statement 总是与一个 PooledConnection 相关联的,ConnectionPoolDataSource 可以用来对池化做一些属性设置。</p>
<h2>11.7 statement 事件</h2>
<p>如果连接池管理器支持 statement 池化,它必须实现 StatementEventListener 接口,然后将自己注册为 PooledConnection 对象的监听者。这个接口定义了以下两个方法,用来监听有可能发生在一个 PreparedStatement 对象上的两种事件。</p>
<ul>
<li>statementClosed --- 当与 PooledConnection 对象相关联的逻辑 PreparedStatement 对象被关闭时触发,也就是说,当应用层调用 PreparedStatement.close 方法时。</li>
<li>statementErrorOccurred --- 当 JDBC 驱动监测到 PreparedStatement 对象不可用时触发。</li>
</ul>
<p>连接池管理器通过 PooledConnection.addStatementEventListener 方法将自己注册为监听者。一般来说,在连接池管理器返回一个 PreparedStatement 对象给应用层使用之前,它必须先把自己注册为一个监听者。<br>当对应的事件发生时,驱动会调用 StatementEventListener 的 statementClosed 方法和 statementErrorOccurred 方法,这两个方法都接收一个 statementEvent 对象作为参数,这个参数就可以用来判断是发生了关闭事件还是异常事件。当 JDBC 应用关闭逻辑 statement ,或者一些错误发生时,JDBC 驱动会调用相关的方法,这个时候,连接池管理器它就可以将这个 statement 放回池子以便重用,或者是抛出异常。</p>
<h2>11.8 ConnectionPoolDataSource 属性</h2>
<p>JDBC 的 API 定义了一系列的属性来设置与池化相关的属性:</p>
<table>
<thead><tr>
<th>属性名</th>
<th>类型</th>
<th>描述</th>
</tr></thead>
<tbody>
<tr>
<td>maxStatements</td>
<td>int</td>
<td>允许池化的最大 statement 数,0 代表不池化</td>
</tr>
<tr>
<td>initialPoolSize</td>
<td>int</td>
<td>当连接池创建时需要创建的初始物理连接数</td>
</tr>
<tr>
<td>minPoolSize</td>
<td>int</td>
<td>连接池最小物理连接数</td>
</tr>
<tr>
<td>maxPoolSize</td>
<td>int</td>
<td>连接池最大物理连接数,0代表无限制</td>
</tr>
<tr>
<td>maxIdleTime</td>
<td>int</td>
<td>连接空闲最大空闲时间,0代表无限制</td>
</tr>
<tr>
<td>propertyCycle</td>
<td>int</td>
<td>属性生效时间,单位为秒</td>
</tr>
</tbody>
</table>
<p>连接池的配置风格遵循 JavaBean 风格。连接池厂商如果需要增加配置属性,那这些新增的属性名不应与已有的标准属性名重复。<br>与 DataSource 的实现一样,ConnectionPoolDataSource 的实现也必须为每个属性增加 setter 和 getter 方法,以下代码是一个示例:</p>
<pre><code>VendorConnectionPoolDS vcp = new VendorConnectionPoolDS();
vcp.setMaxStatements(25);
vcp.setInitialPoolSize(10);
vcp.setMinPoolSize(1);
vcp.setMaxPoolSize(0);
vcp.setMaxIdleTime(0);
vcp.setPropertyCycle(300);</code></pre>
<p>应用服务器会根据设置的属性,来决定应该如何管理相关的池子。<br>ConnectionPoolDataSource 的配置属性无须被 JDBC 客户端直接访问。一些管理工具需要访问的话,建议通过反射的方式。</p>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
简易 RPC 框架
https://segmentfault.com/a/1190000016968628
2018-11-10T02:46:35+08:00
2018-11-10T02:46:35+08:00
ytbean
https://segmentfault.com/u/ytbean
5
<p>原文链接:<a href="https://link.segmentfault.com/?enc=pzkTfgLWQSl9n513kcAxwg%3D%3D.Hsr0bozNJozl3co%2FzrHRxg%2BtdbPGF4aIvzJUEdQ%2FpzqhN5dGl9TnN6KPB2U71kpi" rel="nofollow">《简易 RPC 框架》http://www.ytbean.com/posts/a-simple-rpc/</a></p><h2>需求分析</h2><p>RPC 全称 Remote Procedure Call ,简单地来说,它能让使用者像调用本地方法一样,调用远程的接口,而不需要关注底层的具体细节。</p><p>例如车辆违章代办功能,如果车辆因为某种原因违章,只需要通过这个违章代办功能(它也许是个APP),我们就能动动手指,而省去了一些跑腿的工作。</p><p>不像微服务背景下大家所说的 RPC 框架,如 Dubbo 之类。这个 RPC 框架不提供过多的关于服务注册、服务发现、服务管理等功能。它针对的是这样的一些场景:在内部网络,或者局域网内,两个属于同个业务的系统之间需要通信,而我们又觉得去设计多一种二进制网络协议过于繁琐并且没有必要,这时候如果给客户端开发者一些明确的接口,让他知道实现什么功能该调用什么接口,那么省去的工作量以及开发效率上的提升不言而喻。</p><p>这个 RPC 系统基于 Java 语言实现,需求如下:</p><ul><li>RPC 服务端可以通过一条长连接发布多个接口(Interface),客户端按需生成对应接口的代理。</li><li>RPC 客户端也可以发布接口,以便在必要的时候,服务端可以主动调用客户端的接口实现</li><li>客户端与服务端之间保持长连接并且维持心跳</li><li>服务端针对不同的接口实现,可以指定不同的线程池去处理</li><li>序列化协议支持扩展</li><li>通信协议与具体编程语言无关</li><li>支持并发调用,一个RPC客户端实例要求是线程安全的</li></ul><h2>通信协议设计</h2><p>高效的通信协议一般是二进制格式的,比较常见的还有文本协议比如说HTTP,为了追求效率,这个 RPC 框架就采用二进制格式。</p><h3>协议的基本要素</h3><h4>魔数</h4><p>要了解到,报文是在网络上传输的,安全性比较低,因此有必要采取一些措施使得并不是任何人都可以随随便便往我们的端口上发东西,因此我们对报文要有一个初步的识别功能,这时候“魔数(magic number)”就派上用场了。魔数并不受任何规范约束,没有人可以要求你的魔数应该遵循什么规范,实际上魔数只是我们通信双方都约定的一个“暗号”,不知道这个暗号的人就无法参与进通信中。例如 Java 源文件编译后的 class 文件开头就有一个魔数:0xCAFEBABE,随随便便打开一个class文件用十六进制编辑器查看,就能看到。</p><p><img src="/img/remote/1460000041526286" alt="class文件" title="class文件"></p><p>Java 虚拟机加载 class 的时候会先验证魔数。如果不是 CAFEBABE 就认为是不合法的 class 文件,并拒绝加载。</p><p>不过魔数起到的安全防范作用是非常有限的,“有心人”可以通过抓取网络包就识别出魔数了。因此魔数这个东西其实是“防君子不防小人”。</p><h4>协议版本</h4><p>一个协议可能也会有多个版本,例如说 HTTP1.0 和 HTTP1.1,不同版本的协议元素可能发生了改变,解析方式也会发生改变,因此协议设计这一块,需要预留出地方声明协议的版本,通信双方在解析协议或者拼装协议的时候才有迹可循。</p><h4>报文类型</h4><p>对于RPC框架来说,报文可能有多种类型:心跳类型报文、认证类型报文、请求类型报文、响应类型报文等。</p><h4>上下文 ID</h4><p>RPC 调用其实是一个“请求-响应”的过程,并且跨物理机器,因此每次请求和响应,都必须带上上下文 ID,通信双方才能把请求和响应对应起来。</p><h4>状态</h4><p>状态用来标识一次调用时正常结束还是异常结束,通常由被调用方置状态。</p><h4>请求数据</h4><p>即发送到服务端的调用请求,通常是序列化后的二进制流,长度不定。</p><h4>长度编码字段</h4><p>收报文的一方怎么知道发报文的那一方发了多少字节呢?因此发送方必须在协议里告诉接收方需要接受多少字节才算一个完整的报文。</p><h4>保留字段</h4><p>协议一旦被设计,并非一成不变的,日后可能有变动的可能,因此还需要考虑保留一些字节空间作为保留字段,以备日后协议的扩展。</p><h3>协议设计</h3><p>结合以上的一些设计原则,具体协议设计如下:</p><pre><code> ------------------------------------------------------------------------
| magic (2bytes) | version (1byte) | type (1byte) | reserved (7bits) |
------------------------------------------------------------------------
| status (1byte) | id (8bytes) | body length (4bytes) |
------------------------------------------------------------------------
| |
| body ($body_length bytes) |
| |
------------------------------------------------------------------------
</code></pre><h2>链路可靠性</h2><p>客户端与服务端之间的连接采用 TCP 长连接,一个客户端与服务端之间保持至少一条长连接。接口调用请求的发送,在多条连接之间进行负载均衡。</p><p>每条连接在空闲的时候,由客户端主动向服务端发送心跳报文,并且客户端在发现连接失效或断开的时候,自动进行重连。</p><p>每个客户端向服务端建立连接后,在正式发起接口调用请求之前,都需要进行check in 操作, check in 操作主要是将客户端的身份标识(identifier)和客户端的心跳间隔告诉服务端。利用 netty 的 handler 责任链机制和自带的 IdleStateHandler,自动检测出连接是否空闲,并在空闲时触发心跳报文的发送。而服务端在客户端 checkin 后,根据客户端的心跳频率,在自己的 handler pipeline 上动态加入一个 IdleStateHandler,来检测出客户端是否已经失联,如果是,则主动关闭连接。</p><p>同时,客户端本地将会起一个定时执行任务的线程,定期检查连接是否失效,如果失效,则关闭旧连接,并进行连接的重建。</p><h2>协议编解码</h2><p>前面已经提到协议的设计如下:</p><pre><code> ------------------------------------------------------------------------
| magic (2bytes) | version (1byte) | type (1byte) | reserved (7bits) |
------------------------------------------------------------------------
| status (1byte) | id (8bytes) | body length (4bytes) |
------------------------------------------------------------------------
| |
| body ($body_length bytes) |
| |
------------------------------------------------------------------------
</code></pre><p>协议的解码我们称为 decode,编码我们成为 encode,下文我们将直接使用 decode 和 encode 术语。</p><p>decode 的本质就是讲接收到的一串二进制报文,转化为具体的消息对象,在 Java 中,就是将这串二进制报文所包含的信息,用某种类型的对象存储起来。</p><p>encode 则是将存储了信息的对象,转化为具有相同含义的一串二进制报文,然后网络收发模块再将报文发出去。</p><p>无论是 rpc 客户端还是服务端,都需要有一个 decode 和 encode 的逻辑。</p><h3>消息类型</h3><p>rpc 客户端与服务端之间的通信,需要通过发送不同类型的消息来实现,例如:client 向 server 端发送的消息,可能是请求消息,可能是心跳消息,可能是认证消息,而 server 向 client 发送的消息,一般就是响应消息。</p><p>利用 Java 中的枚举类型,可以将消息类型进行如下定义:</p><pre><code class="java">
/**
* 消息类型
*
* @author beanlam
* @version 1.0
*/
public enum MessageType {
REQUEST((byte) 0x01), HEARTBEAT((byte) 0x02), CHECKIN((byte) 0x03), RESPONSE(
(byte) 0x04), UNKNOWN((byte) 0xFF);
private byte code;
MessageType(byte code) {
this.code = code;
}
public static MessageType valueOf(byte code) {
for (MessageType instance : values()) {
if (instance.code == code) {
return instance;
}
}
return UNKNOWN;
}
public byte getCode() {
return code;
}
}
</code></pre><p>在这个类中设计了 valueOf 方法,方便进行具体的 byte 字节与具体的消息枚举类型之间的映射和转换。</p><h3>调用状态设计</h3><p>client 主动发起的一次 rpc 调用,要么成功,要么失败,server 端有责任告知 client 此次调用的结果,client 也有责任去感知调用失败的原因,因为不一定是 server 端造成的失败,可能是因为 client 端在对消息进行预处理的时候,例如序列化,就已经出错了,这种错误也应该作为一次调用的调用结果返回给 client 调用者。因此引入一个调用状态,与消息类型一样,它也借助了 Java 语言里的枚举类型来实现,并实现了方便的 valueOf 方法:</p><pre><code class="java">
/**
* 调用状态
*
* @author beanlam
* @version 1.0
*/
public enum InvocationStatus {
OK((byte) 0x01), CLIENT_TIMEOUT((byte) 0x02), SERVER_TIMEOUT(
(byte) 0x03), BAD_REQUEST((byte) 0x04), BAD_RESPONSE(
(byte) 0x05), SERVICE_NOT_FOUND((byte) 0x06), SERVER_SERIALIZATION_ERROR(
(byte) 0x07), CLIENT_SERIALIZATION_ERROR((byte) 0x08), CLIENT_CANCELED(
(byte) 0x09), SERVER_BUSY((byte) 0x0A), CLIENT_BUSY(
(byte) 0x0B), SERIALIZATION_ERROR((byte) 0x0C), INTERNAL_ERROR(
(byte) 0x0D), SERVER_METHOD_INVOKE_ERROR((byte) 0x0E), UNKNOWN((byte) 0xFF);
private byte code;
InvocationStatus(byte code) {
this.code = code;
}
public static InvocationStatus valueOf(byte code) {
for (InvocationStatus instance : values()) {
if (code == instance.code) {
return instance;
}
}
return UNKNOWN;
}
public byte getCode() {
return code;
}
}</code></pre><h3>消息实体设计</h3><p>我们将 client 往 server 端发送的统一称为 rpc 请求消息,一个请求对应着一个响应,因此在 client 和 server 端间流动的信息大体上其实就只有两种,即要么是请求,要么是响应。我们将会定义两个类,分别是 RpcRequest 和 RpcResponse 来代表请求消息和响应消息。</p><p>另外由于无论是请求消息还是响应消息,它们都有一些共同的属性,例如说“调用上下文ID”,或者消息类型。因此会再定义一个 RpcMessage 类,作为父类。</p><p><img src="/img/remote/1460000016924448" alt="消息类的关系" title="消息类的关系"></p><h4>RpcMessage</h4><pre><code class="java">
/**
* rpc消息
*
* @author beanlam
* @version 1.0
*/
public class RpcMessage {
private MessageType type;
private long contextId;
private Object data;
public long getContextId() {
return this.contextId;
}
public void setContextId(long id) {
this.contextId = id;
}
public Object getData() {
return this.data;
}
public void setData(Object data) {
this.data = data;
}
public void setType(byte code) {
this.type = MessageType.valueOf(code);
}
public MessageType getType() {
return this.type;
}
public void setType(MessageType type) {
this.type = type;
}
@Override
public String toString() {
return "[messageType=" + type.name() + ", contextId=" + contextId + ", data="
+ data + "]";
}
}</code></pre><h4>RpcRequest</h4><pre><code class="java">import java.util.concurrent.atomic.AtomicLong;
/**
* rpc请求消息
*
* @author beanlam
* @version 1.0
*/
public class RpcRequest extends RpcMessage {
private static final AtomicLong ID_GENERATOR = new AtomicLong(0);
public RpcRequest() {
this(ID_GENERATOR.incrementAndGet());
}
public RpcRequest(long contextId) {
setContextId(contextId);
setType(MessageType.REQUEST);
}
}</code></pre><h4>RpcResponse</h4><pre><code class="java">/**
* rpc响应消息
*
* @author beanlam
* @version 1.0
*/
public class RpcResponse extends RpcMessage {
private InvocationStatus status = InvocationStatus.OK;
public RpcResponse(long contextId) {
setContextId(contextId);
setType(MessageType.RESPONSE);
}
public InvocationStatus getStatus() {
return this.status;
}
public void setStatus(InvocationStatus status) {
this.status = status;
}
@Override
public String toString() {
return "RpcResponse[contextId=" + getContextId() + ", status=" + status.name() + "]";
}
}
</code></pre><h3>netty 编解码介绍</h3><p>netty 是一个 NIO 框架,应该这么说,netty 是一个有良好设计思想的 NIO 框架。一个 NIO 框架必备的要素就是 reactor 线程模型,目前有一些比较优秀而且开源的小型 NIO 框架,例如分库分表中间件 mycat 实现的一个简易 NIO 框架,可以在<a href="[https://github.com/MyCATApache/Mycat-NIO">这里</a>看到。</p><p>netty 的主要特点有:微内核设计、责任链模式的业务逻辑处理、内存和资源泄露的检测等。其中编解码在 netty 中,都被设计成责任链上的一个一个 Handler。</p><p>decode 对于 netty 来说,它提供了 ByteToMessageDecoder,它也提供了 MessageToByteEncoder。</p><p>借助 netty 来实现协议编解码,实际上就是去在这两个handler里面实现编解码的逻辑。</p><h3>decode</h3><p>在实现 decode 逻辑时需要注意的一个问题是,由于二进制报文是在网络上发送的,因此一个完整的报文可能经过多个分组来发送的,什么意思呢,就是当有报文进来后,要确认报文是否完整,decode逻辑代码不能假设收到的报文就是一个完整报文,一般称这为“TCP半包问题”。同样,报文是连着报文发送的,意味着decode代码逻辑还要负责在一长串二进制序列中,分割出一个一个独立的报文,这称之为“TCP粘包问题”。</p><p>netty 本身有提供一些方便的 decoder handler 来处理 TCP 半包和粘包的问题。不过一般情况下我们不会直接去用它,因为我们的协议比较简单,自己在代码里处理一下就可以了。</p><p>完整的 decode 代码逻辑如下所示:</p><pre><code class="java">import cn.com.agree.ats.rpc.message.*;
import cn.com.agree.ats.util.logfacade.AbstractPuppetLoggerFactory;
import cn.com.agree.ats.util.logfacade.IPuppetLogger;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
/**
* 协议解码器
*
* @author beanlam
* @version 1.0
*/
public class ProtocolDecoder extends ByteToMessageDecoder {
private static final IPuppetLogger logger = AbstractPuppetLoggerFactory
.getInstance(ProtocolDecoder.class);
private boolean magicChecked = false;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> list)
throws Exception {
if (!magicChecked) {
if (in.readableBytes() < ProtocolMetaData.MAGIC_LENGTH_IN_BYTES) {
return;
}
magicChecked = true;
if (!(in.getShort(in.readerIndex()) == ProtocolMetaData.MAGIC)) {
logger.warn(
"illegal data received without correct magic number, channel will be close");
ctx.close();
magicChecked = false; //this line of code makes no any sense, but it's good for a warning
return;
}
}
if (in.readableBytes() < ProtocolMetaData.HEADER_LENGTH_IN_BYTES) {
return;
}
int bodyLength = in
.getInt(in.readerIndex() + ProtocolMetaData.BODY_LENGTH_OFFSET);
if (in.readableBytes() < bodyLength + ProtocolMetaData.HEADER_LENGTH_IN_BYTES) {
return;
}
magicChecked = false;// so far the whole packet was received
in.readShort(); // skip the magic
in.readByte(); // dont care about the protocol version so far
byte type = in.readByte();
byte status = in.readByte();
long contextId = in.readLong();
byte[] body = new byte[in.readInt()];
in.readBytes(body);
RpcMessage message = null;
MessageType messageType = MessageType.valueOf(type);
if (messageType == MessageType.RESPONSE) {
message = new RpcResponse(contextId);
((RpcResponse) message).setStatus(InvocationStatus.valueOf(status));
} else {
message = new RpcRequest(contextId);
}
message.setType(messageType);
message.setData(body);
list.add(message);
}
}</code></pre><p>可以看到,我们解决半包问题的时候,是判断有没有收到我们期望收到的报文,如果没有,直接在 decode 方法里面 return,等有更多的报文被收到的时候,netty 会自动帮我们调起 decode 方法。而我们解决粘包问题的思路也很清晰,那就是一次只处理一个报文,不去动后面的报文内容。</p><p>还需要注意的是,在 netty 中,对于 ByteBuf 的 get 是不会消费掉报文的,而 read 是会消费掉报文的。当不确定报文是否收完整的时候,我们都是用 get开头的方法去试探性地验证报文是否接收完全,当确定报文接收完全后,我们才用 read 开头的方法去消费这段报文。</p><h3>encode</h3><p>直接贴代码,参考前文提到的协议格式阅读以下代码:</p><pre><code class="java">/**
*
* 协议编码器
*
* @author beanlam
* @version 1.0
*/
public class ProtocolEncoder extends MessageToByteEncoder<RpcMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out)
throws Exception {
byte status;
byte[] data = (byte[]) rpcMessage.getData();
if (rpcMessage instanceof RpcRequest) {
RpcRequest request = (RpcRequest) rpcMessage;
status = InvocationStatus.OK.getCode();
} else {
RpcResponse response = (RpcResponse) rpcMessage;
status = response.getStatus().getCode();
}
out.writeShort(ProtocolMetaData.MAGIC);
out.writeByte(ProtocolMetaData.VERSION);
out.writeByte(rpcMessage.getType().getCode());
out.writeByte(status);
out.writeLong(rpcMessage.getContextId());
out.writeInt(data.length);
out.writeBytes(data);
}
}
</code></pre><h2>序列化机制</h2><p>在谈 decode 之前,必须先要知道 encode 的过程是什么,它把什么东西转化成了二进制协议。<br>由于我们还未谈到具体的 RPC 调用机制,因此暂且认为 encode 就是把一个包含了调用信息的 Java 对象,从 client 经过序列化,变成一串二进制流,发送到了 server 端。<br>这里需要明确的是,encode 的职责是拼协议,它不负责序列化,同样,decode 只是把整个二进制报文分割,哪部分是报文头,哪部分是报文体,诚然,报文体就是被序列化成二进制流的一个 Java 对象。<br>对于调用方来说,先将调用信息封装成一个 Java 对象,经过序列化后形成二进制流,再经过 encode 阶段,拼接成一个完整的遵守我们定好的协议的报文。<br>对于被调用方来说,则是收取完整的报文,在 decode 阶段将报文中的报文头,报文体分割出来,在序列化阶段将报文体反序列化为一个 Java 对象,从而获得调用信息。</p><h3>基于 netty handler</h3><p>由于这个 RPC 框架基于 netty 实现,因此序列化机制其实体现在了 netty 的 pipeline 上的 handler 上。<br>例如对于调用方,它需要在 pipeline 上加上一个 序列化 encode handler,用来序列化发出去的请求,同时需要加上一个反序列化的 decode handler, 以便反序列化调用结果。如下所示:</p><pre><code class="java"> protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ProtocolEncoder())
.addLast(new ProtocolDecoder())
.addLast(new SerializationHandler(serialization))
.addLast(new DeserializationHandler(serialization));
}</code></pre><p>其中的 SerializationHandler 和 DeserializationHandler 就是上文提到的序列化 encode handler 和反序列化 decode handler。<br>同样,对于被调用方来说,它也需要这两个handler,与调用方的 handler 编排顺序一致。</p><p>其中,serialization 这个参数的对象代表具体的序列化机制策略。</p><h3>序列化机制</h3><p>上文中,SerializationHandler 和 DeserializationHandler 这两个对象都需要一个 serialization 对象作为参数,它是这么定义的:</p><pre><code class="java">private ISerialization serialization = SerializationFactory.getSerialization(ServerDefaults.DEFAULT_SERIALIZATION_TYPE);</code></pre><p>采用工厂模式来创建具体的序列化机制:</p><pre><code class="java">/**
* 序列化工厂
*
* @author beanlam
* @version 1.0
*/
public class SerializationFactory {
private SerializationFactory() {
}
public static ISerialization getSerialization(SerializationType type) {
if (type == SerializationType.JDK) {
return new JdkSerialization();
}
return new HessianSerialization();
}
}</code></pre><p>这里暂时只支持 JDK 原生序列化 和 基于 Hessian 的序列化机制,日后若有其他效率更高更适合的序列化机制,则可以在工厂类中进行添加。</p><blockquote>这里的 hessian 序列化是从 dubbo 中剥离出来的一块代码,感兴趣可以从 dubbo 的源码中的 com.caucho.hessian 包中获得。</blockquote><p>以 HessianSerialization 为例:</p><pre><code class="java">/**
* @author beanlam
* @version 1.0
*/
public class HessianSerialization implements ISerialization {
private ISerializer serializer = new HessianSerializer();
private IDeserializer deserializer = new HessianDeserializer();
@Override
public ISerializer getSerializer() {
return serializer;
}
@Override
public IDeserializer getDeserializer() {
return deserializer;
}
@Override
public boolean accept(Class<?> clazz) {
return Serializable.class.isAssignableFrom(clazz);
}
}</code></pre><p>根据 Hessian 的 API, 分别返回一个 hessian 的序列化器和反序列化器即可。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Java 中的 Monitor 机制
https://segmentfault.com/a/1190000016417017
2018-09-16T19:43:59+08:00
2018-09-16T19:43:59+08:00
ytbean
https://segmentfault.com/u/ytbean
26
<p>原文链接:<a href="https://link.segmentfault.com/?enc=7Ce%2BI11DAPTZ69TL3b1bcw%3D%3D.PGrt8pwJgtSdAQ%2FZYbxSYX1dsvy3g5HmwfI3d0PiM3kj7p%2FxKAbSAMbbS14s3bRn" rel="nofollow">《Java 中的 Monitor 机制》http://www.ytbean.com/posts/monitor-in-java/</a></p><h2>monitor的概念</h2><p>管程,英文是 Monitor,也常被翻译为“监视器”,monitor 不管是翻译为“管程”还是“监视器”,都是比较晦涩的,通过翻译后的中文,并无法对 monitor 达到一个直观的描述。</p><p>在<a href="https://link.segmentfault.com/?enc=yoIy52a%2FGOSDTNsAA7nAmA%3D%3D.ibFikjdwTmY7AdAnxeqkd0VIvjb9UWGxkkLkrM8guRbmxJV7iXArhE1dxVqItzKq" rel="nofollow">《浅析操作系统同步原语》</a> 这篇文章中,介绍了操作系统在面对 进程/线程 间同步的时候,所支持的一些同步原语,其中 semaphore 信号量 和 mutex 互斥量是最重要的同步原语。</p><p>在使用基本的 mutex 进行并发控制时,需要程序员非常小心地控制 mutex 的 down 和 up 操作,否则很容易引起死锁等问题。为了更容易地编写出正确的并发程序,所以在 mutex 和 semaphore 的基础上,提出了更高层次的同步原语 monitor,不过需要注意的是,操作系统本身并不支持 monitor 机制,实际上,monitor 是属于编程语言的范畴,当你想要使用 monitor 时,先了解一下语言本身是否支持 monitor 原语,例如 C 语言它就不支持 monitor,Java 语言支持 monitor。</p><p>一般的 monitor 实现模式是编程语言在语法上提供语法糖,而如何实现 monitor 机制,则属于编译器的工作,Java 就是这么干的。</p><p>monitor 的重要特点是,同一个时刻,只有一个 进程/线程 能进入 monitor 中定义的临界区,这使得 monitor 能够达到互斥的效果。但仅仅有互斥的作用是不够的,无法进入 monitor 临界区的 进程/线程,它们应该被阻塞,并且在必要的时候会被唤醒。显然,monitor 作为一个同步工具,也应该提供这样的管理 进程/线程 状态的机制。想想我们为什么觉得 semaphore 和 mutex 在编程上容易出错,因为我们需要去亲自操作变量以及对 进程/线程 进行阻塞和唤醒。monitor 这个机制之所以被称为“更高级的原语”,那么它就不可避免地需要对外屏蔽掉这些机制,并且在内部实现这些机制,使得使用 monitor 的人看到的是一个简洁易用的接口。</p><h2>monitor 基本元素</h2><p>monitor 机制需要几个元素来配合,分别是:</p><ol><li>临界区</li><li>monitor 对象及锁</li><li>条件变量以及定义在 monitor 对象上的 wait,signal 操作。</li></ol><p>使用 monitor 机制的目的主要是为了互斥进入临界区,为了做到能够阻塞无法进入临界区的 进程/线程,还需要一个 monitor object 来协助,这个 monitor object 内部会有相应的数据结构,例如列表,来保存被阻塞的线程;同时由于 monitor 机制本质上是基于 mutex 这种基本原语的,所以 monitor object 还必须维护一个基于 mutex 的锁。<br>此外,为了在适当的时候能够阻塞和唤醒 进程/线程,还需要引入一个条件变量,这个条件变量用来决定什么时候是“适当的时候”,这个条件可以来自程序代码的逻辑,也可以是在 monitor object 的内部,总而言之,程序员对条件变量的定义有很大的自主性。不过,由于 monitor object 内部采用了数据结构来保存被阻塞的队列,因此它也必须对外提供两个 API 来让线程进入阻塞状态以及之后被唤醒,分别是 wait 和 notify。</p><h2>Java 语言对 monitor 的支持</h2><p>monitor 是操作系统提出来的一种高级原语,但其具体的实现模式,不同的编程语言都有可能不一样。以下以 Java 的 monitor 为例子,来讲解 monitor 在 Java 中的实现方式。</p><h3>临界区的圈定</h3><p>在 Java 中,可以采用 synchronized 关键字来修饰实例方法、类方法以及代码块,如下所示:</p><pre><code class="java">
/**
* @author beanlam
* @version 1.0
* @date 2018/9/12
*/
public class Monitor {
private Object ANOTHER_LOCK = new Object();
private synchronized void fun1() {
}
public static synchronized void fun2() {
}
public void fun3() {
synchronized (this) {
}
}
public void fun4() {
synchronized (ANOTHER_LOCK) {
}
}
}</code></pre><p>被 synchronized 关键字修饰的方法、代码块,就是 monitor 机制的临界区。</p><h3>monitor object</h3><p>可以发现,上述的 synchronized 关键字在使用的时候,往往需要指定一个对象与之关联,例如 synchronized(this),或者 synchronized(ANOTHER_LOCK),synchronized 如果修饰的是实例方法,那么其关联的对象实际上是 this,如果修饰的是类方法,那么其关联的对象是 this.class。总之,synchronzied 需要关联一个对象,而这个对象就是 monitor object。<br>monitor 的机制中,monitor object 充当着维护 mutex以及定义 wait/signal API 来管理线程的阻塞和唤醒的角色。<br>Java 语言中的 java.lang.Object 类,便是满足这个要求的对象,任何一个 Java 对象都可以作为 monitor 机制的 monitor object。<br>Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据和对齐填充,而在其对象头中,保存了锁标识;同时,java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制,基本原理如下所示:</p><p><img src="/img/remote/1460000041526493" alt="MonitorObject.png" title="MonitorObject.png"></p><p>当一个线程需要获取 Object 的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 wait set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。这个外部条件在 monitor 机制中称为条件变量。</p><h3>synchronized 关键字</h3><p>synchronized 关键字是 Java 在语法层面上,用来让开发者方便地进行多线程同步的重要工具。要进入一个 synchronized 方法修饰的方法或者代码块,会先获取与 synchronized 关键字绑定在一起的 Object 的对象锁,这个锁也限定了其它线程无法进入与这个锁相关的其它 synchronized 代码区域。</p><p>网上很多文章以及资料,在分析 synchronized 的原理时,基本上都会说 synchronized 是基于 monitor 机制实现的,但很少有文章说清楚,都是模糊带过。<br>参照前面提到的 Monitor 的几个基本元素,如果 synchronized 是基于 monitor 机制实现的,那么对应的元素分别是什么?<br>它必须要有临界区,这里的临界区我们可以认为是对对象头 mutex 的 P 或者 V 操作,这是个临界区<br>那 monitor object 对应哪个呢?mutex?总之无法找到真正的 monitor object。<br>所以我认为“synchronized 是基于 monitor 机制实现的”这样的说法是不正确的,是模棱两可的。<br>Java 提供的 monitor 机制,其实是 Object,synchronized 等元素合作形成的,甚至说外部的条件变量也是个组成部分。JVM 底层的 ObjectMonitor 只是用来辅助实现 monitor 机制的一种常用模式,但大多数文章把 ObjectMonitor 直接当成了 monitor 机制。<br>我觉得应该这么理解:Java 对 monitor 的支持,是以机制的粒度提供给开发者使用的,也就是说,开发者要结合使用 synchronized 关键字,以及 Object 的 wait / notify 等元素,才能说自己利用 monitor 的机制去解决了一个生产者消费者的问题。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
浅析操作系统同步原语
https://segmentfault.com/a/1190000016416989
2018-09-16T19:40:47+08:00
2018-09-16T19:40:47+08:00
ytbean
https://segmentfault.com/u/ytbean
4
<h2>概述</h2><p>原文链接:<a href="https://link.segmentfault.com/?enc=r%2BliZ1f3UZToUi696lpQIQ%3D%3D.69%2FEiz2NSjroUBiRuFxqaTJLyHwRfk1kgGeRmGHwPYJ%2FEvqp6dzTe8OD9feb4ON%2B" rel="nofollow">《浅谈操作系统同步原语》http://www.ytbean.com/posts/syncprimitive/</a></p><p>日常开发中,我们经常会碰到并发编程,我们使用的大多数编程语言,基本上都为我们提供了并发编程的 API,例如 Java 中的 <code>Thread</code>,Golang 中的 <code>Goroutine</code>。</p><p>然而编程语言的 API 仅仅是提供了”糖“,想要深入了解并发的原理,仅仅停留在 API 层面是不够的,还需要了解操作系统底层的同步原理。一般的操作系统都会提供基本的同步原语,高级语言在底层利用这些同步原语,向开发者提供并发编程的 API。</p><p>并发编程往往避不开一个问题,那就是如何让多个运行中的线程好好相处,在发生资源竞争的时候,按照程序员设想的方式运行。</p><h2>竞态条件</h2><p>并发编程的话题往往都是从竞态条件(Race Condition)这个话题开始的。</p><p>在一般的操作系统中,不同的进程可能会分享一块公共的存储区域,例如内存或者是硬盘上的文件,这些进程都允许在这些区域上进行读写。</p><p>操作系统有一些职责,来协调这些使用公共区域的进程之间以正确的方式进行想要的操作;这些进程之间需要通信,需要互相沟通,有商有量,才能保证一个进程的动作不会影响到另外一个进程正常的动作,进而导致进程运行后得不到期望的结果。</p><p>在操作系统概念中,通常用 IPC(Inter Process Communication,即进程间通信)这个名词来代表多个进程之间的通信。</p><p>为了解释什么是竞态条件(race condition),我们引入一个简单的例子来说明:</p><blockquote><p>一个文件中保存了一个数字 n,进程 A 和进程 B 都想要去读取这个文件的数字,并把这个数字加 1 后,保存回文件。</p><p>假设 n 的初始值是 0,在我们理想的情况下,进程 A 和进程 B 运行后,文件中 n 的值应该为 2,但实际上可能会发生 n 的值为 1。</p></blockquote><p>我们可以考量一下,每个进程做这件事时,需要经过什么步骤:</p><ol><li>读取文件里 n 的值</li><li>令 n = n + 1</li><li>把新的 n 值保存回文件</li></ol><p><img src="/img/remote/1460000041525241" alt="竞态条件" title="竞态条件"></p><p>在进一步解释竞态条件之前,必须先回顾操作系统概念中的几个知识点:</p><ol><li>进程是可以并发运行的,即使只有一个 CPU 的时候)</li><li>操作系统的时钟中断会引起进程运行的重新调度,</li><li>除了时钟中断,来自其它设备的中断也会引起进程运行的重新调度</li></ol><p>假设进程 A 在运行完步骤 1 和 2,但还没开始运行步骤 3 时,发生了一个时钟中断,这个时候操作系统通过调度,让进程 B 开始运行,进程 B 运行步骤 1 时,发现 n 的值为 0,于是它运行步骤 2 和 3,最终会把 n = 1 保存到文件中。之后进程 A 继续运行时,由于它并不知道在它运行步骤 3 之前,进程 B 已经修改了文件里的值,所以进程 A 也会把 n = 1 写回到文件中。这就是问题所在,进程 A 在运行的过程中,会有别的进程去操作它所操作的数据。</p><p>唯一能让 n = 2 的方法,只能期望进程 A 和进程 B <strong>按顺序</strong>分别完整地运行完所有步骤。因此我们把竞态条件进行如下定义:</p><blockquote>两个或者多个进程读写某些共享数据,而最后的结果取决于进程运行的准确时序,称为竞态条件。</blockquote><p>在上述的文字中,我们使用进程作为对象来讨论竞态条件,实际上对于线程也同样适用,这里的线程包含但不限于内核线程、用户线程。因为在操作系统中,进程其实是依靠线程来运行程序的。更甚至,在 Java 语言的线程安全中,竞态条件这个概念也同样适用。(可以参考<a href="https://link.segmentfault.com/?enc=tVaBuFAnpL748LhYskA3rQ%3D%3D.tsvN0diDOHr%2F5AFM%2BJ0AKucHMDSFvaUkMug6NAnL9ezE9RcDuxc9UvhGQN6LpffZ" rel="nofollow">《Java 并发编程实战》</a>第二章)</p><h2>互斥与临界区</h2><p>如何避免 race condition,我们需要以某种手段,确保当一个进程在使用一个共享变量或者文件时,其它的进程不能做同样的操作,换言之,我们需要“互斥”。</p><p><img src="/img/remote/1460000041525242" alt="临界区" title="临界区"></p><p>回顾上述例子,我们可以把步骤 1 - 3 这段程序片段定义为临界区。</p><p>临界区意味着这个区域是敏感的,因为一旦进程运行到这个区域,那么意味着会对公共数据区域或者文件进行操作,也意味着有可能有其它进程也正运行到了临界区。如果能够采用适当的方式,使得这两个进程不会同时处于临界区,那么就能避免竞态条件。</p><p>也就是说,我们需要想想怎么样做到“互斥”。</p><h2>如何互斥</h2><p>互斥的本质就是阻止多个进程同时进入临界区。</p><h3>屏蔽中断</h3><p>之前提到的例子中,进程 B 之所以能够进入临界区,是因为进程 A 在临界区中受到了中断。</p><p>如果我们让进程 A 在进入临界区后,立即对所有中断进行屏蔽,离开临界区后,才响应中断,那么即使发生中断,那么 CPU 也不会切换到其它进程,因此此时进程 A 可以放心地修改文件内容,不用担心其它的进程会干扰它的工作。</p><p><img src="/img/remote/1460000041525243" alt="屏蔽中断" title="屏蔽中断"></p><p>然而,这个设想是美好,实际上它并不可行。</p><p>一方面,如果有多个CPU,那么进程 A 无法对其它 CPU 屏蔽中断,它只能屏蔽正在调度它的 CPU,因此由其它 CPU 调度的进程,依然可以进入临界区;</p><p>另一方面,关于权力的问题,是否可以把屏蔽中断的权力交给用户进程?如果这个进程屏蔽中断后再也不响应中断了, 那么一个进程有可能挂住整个操作系统。</p><h3>锁变量</h3><p>也许可以通过设置一个锁标志位,将其初始值设置为 0 ,当一个进程想进入临界区时,先检查锁的值是否为 0,如果为 0,则设置为 1,然后进入临界区,退出临界区后把锁的值改为0;</p><p>若检查时锁的值已经为1,那么代表其他进程已经在临界区中了,于是进程进行循环等待,并不断检测锁的值,直到它变为0。</p><p><img src="/img/remote/1460000041525244" alt="锁变量" title="锁变量"></p><p>但这种方式也存在着竞态条件,原因是,当一个进程读出锁的值为0时,在它将其值设置为1之前,另一个进程被调度起来,它也读到锁的值为0,这样就出现了两个进程同时都在临界区的情况。</p><h3>严格轮换法</h3><p>锁变量之所以出问题,其实是因为将锁变量由0改为1这个动作,是由想要获取锁的进程去执行的。如果我们把这个动作改为由已经获得锁的进程去执行,那么就不存在竞态条件了。</p><p>先设置一个变量 turn,代表当前允许谁获得锁,假设有两个进程,进程 A 的逻辑如下所示:</p><pre><code class="java"> while (turn != 0){// 如果还没轮到自己获取锁,则进入循环等待
}
do_critical_region();// 执行临界区的程序
turn = 1;// 由获得锁的一方将锁变量修改为其它值,允许其它进程获得锁
do_non_critical_region();// 执行非临界区的程序</code></pre><p>进程 B 的逻辑如下所示:</p><pre><code class="java"> while (turn != 1) {// 如果还没轮到自己获取锁,则进入循环等待
}
do_critical_region();// 执行临界区的程序
turn = 0;// 由获得锁的一方将锁变量修改为其它值,允许其它进程获得锁
do_non_critical_region();// 执行非临界区的程序</code></pre><p>但这里需要考虑到一个事情,假设进程 A 的 do_non_critical_region() 需要执行很长时间,即进程 A 的非临界区的逻辑需要执行较长时间,而进程 B 的非临界区的逻辑很快就执行完,显然,进程 A 进入临界区的频率会比进程 B 小一点。</p><p>理想的情况下,进程 B 应该多进入临界区几次。但是由于进程 B 在执行非临界区逻辑前会把 turn 设置为 0,等它很快地把非临界区的逻辑执行完后,回来检查 turn 的值时,发现 turn 的值一直都不是 1,turn 的值需要进程 A 把它设置为 1,而进程 A 此时却正在进行着漫长的非临界区逻辑代码,所以导致进程 B 无法进入临界区。</p><p>这就说明,在一个进程比另一个进程慢很多的情况下,严格轮换法并不是一个好办法。</p><h3>Peterson 算法</h3><p>严格轮换法的问题就出在严格两个字上。</p><p>多个进程之间是轮流进入临界区的,根本原因是想要获得锁的进程需要依赖其它进程对于锁变量的修改,而其它进程都要先经过非临界区逻辑的执行才会去修改锁变量。</p><p>严格轮换法中的 turn 变量不仅用来表示当前该轮到谁获取锁,而且它的值未改变之前,都意味着它依然阻止了其它进程进入临界区,恰恰好,一个进程总是要经过非临界区的逻辑后才会去改变turn的值。</p><p>因此,我们可以用两个变量来表示,一个变量表示当前应该轮到谁获得锁,另一个变量表示当前进程已经离开了临界区,这种方法实际上叫做 Peterson 算法,是由荷兰数学家 T.Dekker 提出来的。</p><pre><code class="java"> static final int N = 2;
int turn = 0;
boolean[] interested = new boolean[N];
void enter_region(int process) {
int other = 1 - process;
interested[process] = true;
turn = process;
while(turn == process && !interested[other]) {
}
}
void exit_region(int process) {
interested[process] = false;
}</code></pre><p>进程在需要进入临界区时,先调用 enter_region,离开临界区后,调用 exit_region。</p><p>Peterson 算法使得进程在离开临界区后,立马消除了自己对于临界区的“兴趣”,因此其它进程完全可以根据 turn 的值,来判断自己是否可以合法进入临界区。</p><h3>TSL 指令</h3><p>回顾一下我们之前提到的“锁变量”这种方法,它的一个致命的缺点是对状态变量进行改变的时候,如从 0 改为 1 或者从 1 改为 0 时,是可以被中断打断的,因此存在竞态条件。</p><p>之后我们在锁变量的基础上,提出如果锁变量的修改不是由想要获取进入临界区的进程来修改,而是由已经进入临界区后想要离开临界区的进程来修改,就可以避免竞态条件,继而引发出严格轮换法,以及从严格轮换法基础上改进的 Peterson 算法。</p><p>这些方法都是从软件的方式去考虑的。实际上,可以在硬件 CPU 的支持下,保证锁变量的改变不被打断,使锁变量成为一种很好的解决进程互斥的方法。</p><p>目前大多数的计算机的 CPU,都支持 TSL 指令,其全称为 Test and Set Lock,它将一个内存的变量(字)读取寄存器 RX 中,然后再该内存地址上存一个非零值,读取操作和写入操作从硬件层面上保证是不可打断的,也就是说是原子性的。</p><p>它采用的方式是在执行 TSL 指令时锁住内存总线,禁止其它 CPU 在 TSL 指令结束之前访问内存。这也是我们常说的 CAS (Compare And Set)。</p><p>当需要把锁变量从 0 改为 1 时,先把内存的值复制到寄存器,并将内存的值设置为 1,然后检查寄存器的值是否为 0,如果为 0,则操作成功,如果非 0 ,则重复检测,知道寄存器的值变为 0,如果寄存器的值变为 0 ,意味着另一个进程离开了临界区。进程离开临界区时,需要把内存中该变量的值设置为 0。</p><h2>等待临界区</h2><h3>忙等待</h3><p>上述提到的 Peterson 算法,以及 TSL 方法,实际上它们都有一个特点,那就是在等待进入临界区的时候,它们采用的是忙等待的方式,我们也常常称之为自旋。它的缺点是浪费 CPU 的时间片,并且会导致<strong>优先级反转</strong>的问题。</p><blockquote>考虑一台计算机有两个进程, H 优先级较高,L 优先级较低。调度规则规定,只要 H 处于就绪状态,就可以运行。在某一时刻,L 处于临界区内,此时 H 处于就绪态,准备运行。但 H 需要进行忙等待,而 L 由于优先级低,没法得到调度,因此也无法离开临界区,所以 H 将会永远忙等待,而 L 总无法离开临界区。这种情况称之为<strong>优先级反转问题</strong>(priority inversion problem)</blockquote><h3>阻塞与唤醒</h3><p>操作系统提供了一些原语,sleep 和 wakeup。</p><blockquote>内核提供给核外调用的过程或者函数成为原语(primitive),原语在执行过程中不允许中断。</blockquote><p>sleep 是一个将调用进程阻塞的系统调用,直到另外一个进程调用 wakeup 方法,将被阻塞的进程作为参数,将其唤醒。阻塞与忙等待最大的区别在于,进程被阻塞后CPU将不会分配时间片给它,而忙等待则是一直在空转,消耗 CPU 的时间片。</p><h2>信号量</h2><p>首先需要明白的一点是,信号量的出现是为了解决什么问题。</p><p>由于一个进程的阻塞和唤醒是在不同的进程中造成的,比如说进程 A 调用了 sleep() 会进入阻塞,而进程 B 调用 wakeup(A)会把进程 A 唤醒,因为是在不同的进程中进行的,所以也存在着被中断的问题。</p><p>假如进程 A 根据逻辑,需要调用 sleep() 进入阻塞状态,然而,就在它调用 sleep 方法之前,由于时钟中断,进程 B 开始运行了,根据逻辑,它调用了 wakeup() 方法唤醒进程 A,可是由于进程 A 还未进入阻塞状态,因此这个 wakeup 信号就丢失了,等待进程 A 从之前中断的位置开始继续运行时并进入阻塞后,可能再也没有进程去唤醒它了。</p><p>因此,进程的阻塞和唤醒,应该需要额外引进一个变量来记录,这个变量记录了唤醒的次数,每次被唤醒,变量的值加1。有了这个变量,即使wakeup操作先于sleep操作,但wakeup操作会被记录到变量中,当进程进行sleep时,因为已经有其它进程唤醒过了,此时认为这个进程不需要进入阻塞状态。</p><p>这个变量,在操作系统概念中,则被称为信号量(semaphore),由 Dijkstra 在 1965 年提出的一种方法。</p><p><img src="/img/remote/1460000041525245" alt="信号量" title="信号量"></p><p>对信号量有两种操作, down 和 up。</p><p>down 操作实际上对应着 sleep,它会先检测信号量的值是否大于0,若大于0,则减1,进程此时无须阻塞,相当于消耗掉了一次 wakeup;若信号量为0,进程则会进入阻塞状态。</p><p>而 up 操作对应着 wakeup,进行 up 操作后,如果发现有进程阻塞在这个信号量上,那么系统会选择其中一个进程将其唤醒,此时信号量的值不需要变化,但被阻塞的进程已经少了一个;如果 up 操作时没有进程阻塞在信号量上,那么它会将信号量的值加1。</p><p>有些地方会把 down 和 up 操作称之为 PV 操作,这是因为在 Dijkstra 的论文中,使用的是 P 和 V 分别来代表 down 和 up。</p><p>信号量的 down 和 up 操作,是操作系统支持的原语,它们是具有原子性的操作,不会出现被中断的情况。</p><h2>互斥量</h2><p>互斥量(mutex)其实是信号量的一种特例,它的值只有 0 和 1,当我们不需要用到信号量的计数能力时,我们可以使用互斥量,实际上也意味着临界区值同一时间只允许一个进程进入,而信号量是允许多个进程同时进入临界区的。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Java 线程的实现方式
https://segmentfault.com/a/1190000016416976
2018-09-16T19:38:37+08:00
2018-09-16T19:38:37+08:00
ytbean
https://segmentfault.com/u/ytbean
7
<p>原文链接:<a href="https://link.segmentfault.com/?enc=887ibC3%2FS%2F0NnODmd8l2wQ%3D%3D.jxocEHLMIdf%2B5KLzmTErdMi0KZiw035P05YqST%2B%2BcY6G%2ByR0zCLdH3UnKjkduxjZ" rel="nofollow">《Java 线程的实现方式》http://www.ytbean.com/posts/java-thread-impl/</a></p><h2>进程与线程</h2><p>在传统的操作系统中,最核心的概念是“进程”,进程是对正在运行的程序的一个抽象。<br>进程的存在让“并行”成为了可能,在一个操作系统中,允许运行着多个进程,这些进程“看起来”是同时在运行的。<br>如果我们的计算机同时运行着 web 浏览器、电子邮件客户端、即时通讯软件例如QQ微信等多个进程,我们感觉这些进程都是同时在运行的,假设这台计算机搭配的是多个 CPU 或者 多核 CPU,那么这种多个进程并行的现象可能一点也不奇怪,完全可以为每个进程单独分配一个 CPU,这样就实现了多进程并行。<br>然而事实上,在计算机只有一个 CPU 的情况下,它也能给人类一种感觉:多个进程同时在运行。但人类的感觉往往是比较模糊的,不精确的。事实是由于 CPU 的计算速度非常地快,它能快速地在各个进程之间切换,在某一瞬间,CPU 只能运行一个进程,但一秒钟之内,它就能通过快速切换,让人产生多个进程同时在运行的错觉。<br>在操作系统中,为什么在进程的基础上,又衍生出了线程的概念呢?</p><ol><li>由于对于一些进程而言,它内部会发生多种活动,有些活动可能会在某个时间里阻塞,有些活动不会,如果通过线程将这些活动分离开使它们能够并行地运行,则设计程序的时候会更加简单。</li><li>线程比进程的创建更加轻量级,性能消耗更少</li><li>如果一个进程既需要 CPU 计算,也需要I/O处理,拥有多线程允许这些活动重叠进行,加快整个进程的执行速度。</li></ol><p>每一个进程在操作系统中都拥有独立的一块内存地址空间,该进程创建的所有线程共享这块内存,支持多线程的操作系统,会让线程作为 CPU 调度的最小单位。CPU 的时间片在不同的线程之间进行分配。</p><h2>线程的可能实现方式</h2><p>基本上主流的操作系统都支持线程,也提供了线程的实现。而 Java 语言为了应对不同硬件和操作系统的差异,提供了对线程操作的统一抽象,在 Java 中我们使用 Thread 类来代表一个线程。<br>Thread 的具体实现可能会有不同的实现方式:</p><h3>使用内核线程实现</h3><p>内核线程是操作系统内核支持的线程,在内核中有一个线程表用来记录系统中的所有线程,创建或者销毁一个线程时,都需要涉及到系统调用,然后再内核中对线程表进行更新操作。对内核线程的阻塞以及其它操作,都涉及到系统调用,系统调用的代价都比较大,涉及到在用户态和内核态之间的来回切换。此外,内核内部有线程调度器,用于决定应该将 CPU 时间片分配个哪个线程。</p><p><img src="/img/remote/1460000041526555" alt="image-20220310153848224" title="image-20220310153848224"></p><p>程序一般不会直接操作内核线程,而是使用内核线程的一种高级接口,轻量级进程。轻量级进程与内核线程之间的关系是 1:1,每一个轻量级进程内部都有一个内核线程支持。</p><p><img src="/img/remote/1460000041526556" alt="内核线程实现.png" title="内核线程实现.png"></p><p>上图中, LWP 指 Light Weight Process,即轻量级进程;KLT 指 Kernel Level Thread,即内核线程。</p><h3>使用用户线程实现</h3><p>用户线程是程序或者编程语言自己实现的线程库,系统内核无法感知到这些线程的存在。用户线程的建立、同步、销毁和调度,都在用户态中完成,无须内核的帮助,不需要进行系统调用,这样的好处是对于线程的操作是非常高效的。在这种情况下,进程和用户线程的比例是 1 :N。</p><p><img src="/img/remote/1460000041526557" alt="用户态线程实现.png" title="用户态线程实现.png"></p><p>用户态线程面对如何阻塞线程时,会面临困难,阻塞一个用户态线程会出现把整个进程都阻塞的情况,多线程也就失去了意义。因为缺少内核的支持,所以很多需要利用内核才能完成的工作,例如阻塞与唤醒线程、多 CPU 环境下线程的映射等,都需要用户程序去实现,实现起来会异常困难。</p><p><img src="/img/remote/1460000041526558" alt="image-20220310153919186" title="image-20220310153919186"></p><h3>使用用户线程和内核线程混合实现</h3><p><img src="/img/remote/1460000041526559" alt="image-20220310153937199" title="image-20220310153937199"></p><p>在这种混合实现下,既存在用户线程,也存在内核线程。用户态线程的创建、切换这些操作依然很高效,并且用户态实现的线程,比较容易加大线程的规模。需要操作系统内核支持的功能,则通过内核线程来做到,例如映射到不同的处理器上、处理线程的阻塞与唤醒以及内核线程的调度等。这种实现依然会使用到轻量级进程 LWP,它是用户线程和内核线程之间的桥梁。</p><p><img src="/img/remote/1460000041526560" alt="混合实现.png" title="混合实现.png"></p><h2>Java 线程的实现</h2><p>在 JDK1.2 之前, Java 的线程是使用用户线程实现的,在 JDK1.2 之后,Java 才采用操作系统原生支持的线程模型来实现,Java 线程模型的实现方式,主要取决于操作系统。对于 Oracle JDK 来说,在 Windows 和 Linux 上的线程模型是采用一对一的方式实现的,即一条 Java 线程映射到一条轻量级进程(内核线程)。而在 Solaris 平台中,Java 则支持 1 :1 和 N : M 的线程模型。</p><h2>线程的阻塞与等待</h2><p>Java 中的线程的状态有以下几种:New,Runnable,Waiting, TimedWaiting, Blocked,Terminated。</p><p><img src="/img/remote/1460000041526561" alt="线程状态.png" title="线程状态.png"></p><ul><li>NEW:线程初创建,未运行</li><li>RUNNABLE:线程正在运行,但不一定消耗 CPU</li><li>BLOCKED:线程正在等待另外一个线程释放锁</li><li>WAITING:线程执行了 wait, join, LockSupport.park() 方法</li><li>TIMED_WAITING:线程调用了sleep, wait, join, LockSupport.parkNanos() 等方法,与 WAITING 状态不同的是,这些方法带有表示时间的参数。</li></ul><p>其中 Blocked 和 Waiting 有个重要的区别是,阻塞(Blocked)状态的线程在等待获取一个排他锁,例如线程在等待进入一个synchronized关键字包围的临界区时,就进入 Blocked 状态。而 Waiting 状态则是在等待被唤醒,或者等待一段时间。</p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=k99Vap0Bfq%2FN7VLwzVJ24g%3D%3D.3P1GTX7mFF3s40ck3Zu76fP0W0s6xY3bHTz7dBHkOsjdbI%2F8Jjm4riDIDGpJBsQ9" rel="nofollow">《深入理解 Java 虚拟机》第二版 - 周志明</a><br><a href="https://link.segmentfault.com/?enc=HSSNoHCJR2ufT%2FWayiM9hw%3D%3D.N32PjziWvVW3mMXsei4VV5QnrS5vHuhC966cfPN6T6YfsFd5UxRcDakey2q8rW8d" rel="nofollow">《现代操作系统》第四版 - Andrew S. Tanenbaum</a></p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
线程安全实现与 CLH 队列
https://segmentfault.com/a/1190000016416947
2018-09-16T19:34:55+08:00
2018-09-16T19:34:55+08:00
ytbean
https://segmentfault.com/u/ytbean
1
<p>原文链接:<a href="https://link.segmentfault.com/?enc=MxcYnxYHkc1Am8YbDUh9kg%3D%3D.HqWgUSx7AIySdr%2FLMCXJCFAZV%2B7V402OnIQsBFtpVwGPBVVVPHIoNXw5oYmsAsmY" rel="nofollow">《线程安全实现与 CLH 队列》http://www.ytbean.com/posts/clh-queue/</a></p><h2>阻塞同步</h2><p>在 Java 中,我们经常使用 synchronized 关键字来做到互斥同步以解决多线程并发访问共享数据的问题。synchronzied 关键字在编译后,会在 synchronized 所包含的同步代码块前后分别加入 monitorenter 和 monitorexit 这两个字节码指令。synchronized 关键字需要指定一个对象来进行加锁和解锁。例如:</p><pre><code class="java">public class Main {
private static final Object LOCK = new Object();
public static void fun1() {
synchronized (LOCK) {
// do something
}
}
public static void fun2() {
synchronized (LOCK) {
// do something
}
}
}</code></pre><p>在没有明确指定该对象时,根据 synchonized 修饰的是实例方法还是静态方法,从而决定是采用对象实例或者类的class实例作为所对象。例如:</p><pre><code class="java">public class SynchronizedTest {
public synchronized void doSomething() {
//采用实例对象作为锁对象
}
}</code></pre><pre><code class="java">public class SynchronizedTest {
public static synchronized void doSomething() {
//采用SynchronizedTest.class 实例作为锁对象
}
}</code></pre><p>由于基于 synchronized 实现的阻塞互斥,除了需要阻塞操作线程,而且唤醒或者阻塞操作系统级别的原生线程,需要从用户态转换到内核态中,这个状态的转换消耗的时间可能比用户代码执行的时间还要长,因此我们经常说 synchronized 是 Java 语言中的 “重量级锁”。</p><h2>非阻塞同步</h2><h3>乐观锁与悲观锁</h3><p>使用 synchronized 关键字的同步方式最主要的问题就是进行线程阻塞和唤醒时所带来的的性能消耗问题。阻塞同步属于悲观的并发策略,只要有可能出现竞争,它都认为一定要加锁。<br>然而同步策略还有另外一种乐观的策略,乐观并发策略先进性对数据的操作,如果没有发现其它线程也操作了数据,那么就认为这个操作是成功的。如果发生了其它线程也操作了数据,那么一般采取不断重试的手段,直到成功为止,这种乐观锁的策略,不需要把线程阻塞,属于非阻塞同步的一种手段。</p><h3>CAS</h3><p>乐观并发策略主要有两个重要的阶段,一个是对数据进行操作,另外一个是进行冲突的检测,即检测其它线程有无同时也对该数据进行了操作。这里的数据操作和冲突检测需要具备<strong>原子性</strong>,否则就容易出现类似于 i++ 的问题。<br>CAS 的含义为 compare and swap,目前绝大多数 CPU 都原生支持 CAS 原子指令,例如在 IA64、x86的指令集中,就有 cmpxchg 这样的指令来完成 CAS 功能,它的原子性要求是在硬件层面上得到保证的。<br>CAS 指令一般需要有三个参数,分别是值的内存地址、期望中的旧值和新值。CAS 指令执行时,如果该内存地址上的值符合期望中的旧值,处理器会用新值更新该内存地址上的值,否则就不更新。这个操作在 CPU 内部保证了是原子性的。<br>在 Java 中有许多 CAS 相关的 API,我们常见的有 <code>java.util.concurrent</code> 包下的各种原子类,例如<code>AtomicInteger</code>,<code>AtomicReference</code>等等。<br>这些类都支持 CAS 操作,其内部实际上也依赖于 <code>sun.misc.Unsafe</code> 这个类里的 compareAndSwapInt() 和 compareAndSwapLong() 方法。<br>CAS 并非是完美无缺的,尽管它能保证原子性,但它存在一个著名的 <strong>ABA</strong> 问题。一个变量初次读取的时候值为 A,再一次读取的时候也为 A,那么我们是否能说明这个变量在两次读取中间没有发生过变化?不能。在这期间,变量可能由 A 变为 B,再由 B 变为 A,第二次读取的时候看到的是 A,但实际上这个变量发生了变化。一般的代码逻辑不会在意这个 ABA 问题,因为根据代码逻辑它不会影响并发的安全性,但如果在意的话,可能考虑采用阻塞同步的方式而不是 CAS。实际上 JDK 本身也对这个 ABA 问题解决方案,提供了 <code>AtomicStampedReference</code> 这个类,为变量加上版本来解决 ABA 问题。</p><h2>自旋锁</h2><p>以 synchronized 为代表的阻塞同步,因为阻塞线程会恢复线程的操作都需要涉及到操作系统层面的用户态和内核态之间的切换,这对系统的性能影响很大。自旋锁的策略是当线程去获取一个锁时,如果发现该锁已经被其它线程占有,那么它不马上放弃 CPU 的执行时间片,而是进入一个“无意义”的循环,查看该线程是否已经放弃了锁。<br>但自旋锁适用于<strong>临界区</strong>比较小的情况,如果锁持有的时间过长,那么自旋操作本身就会白白耗掉系统的性能。</p><p>以下为一个简单的自旋锁实现:</p><pre><code class="java">import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread currentThread = Thread.currentThread();
// 如果锁未被占用,则设置当前线程为锁的拥有者
while (!owner.compareAndSet(null, currentThread)) {}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有锁的拥有者才能释放锁
owner.compareAndSet(currentThread, null);
}
}</code></pre><p>上述的代码中, owner 变量保存获得了锁的线程。这里的自旋锁有一些缺点,第一个是没有保证公平性,等待获取锁的线程之间,无法按先后顺序分别获得锁;另一个,由于多个线程会去操作同一个变量 owner,在 CPU 的系统中,存在着各个 CPU 之间的缓存数据需要同步,保证一致性,这会带来性能问题。</p><h3>公平的自旋</h3><p>为了解决公平性问题,可以让每个锁拥有一个服务号,表示正在服务的线程,而每个线程尝试获取锁之前需要先获取一个排队号,然后不断轮询当前锁的服务号是否是自己的服务号,如果是,则表示获得了锁,否则就继续轮询。下面是一个简单的实现:</p><pre><code class="java">import java.util.concurrent.atomic.AtomicInteger;
public class TicketLock {
private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
private AtomicInteger ticketNum = new AtomicInteger(); // 排队号
public int lock() {
// 首先原子性地获得一个排队号
int myTicketNum = ticketNum.getAndIncrement();
// 只要当前服务号不是自己的就不断轮询
while (serviceNum.get() != myTicketNum) {
}
return myTicketNum;
}
public void unlock(int myTicket) {
// 只有当前线程拥有者才能释放锁
int next = myTicket + 1;
serviceNum.compareAndSet(myTicket, next);
}
}</code></pre><p>虽然解决了公平性的问题,但依然存在前面说的多 CPU 缓存的同步问题,因为每个线程占用的 CPU 都在同时读写同一个变量 serviceNum,这会导致繁重的系统总线流量和内存操作次数,从而降低了系统整体的性能。</p><h3>MCS 自旋锁</h3><p>MCS 的名称来自其发明人的名字:John Mellor-Crummey和Michael Scott。<br>MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:</p><pre><code class="java">import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isWaiting = true; // 默认是在等待锁
}
volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MCSNode.class, "queue");
public void lock(MCSNode currentThread) {
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
if (predecessor != null) {
predecessor.next = currentThread;// step 2
while (currentThread.isWaiting) {// step 3
}
} else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
currentThread.isWaiting = false;
}
}
public void unlock(MCSNode currentThread) {
if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义
return;
}
if (currentThread.next == null) {// 检查是否有人排在自己后面
if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
// compareAndSet返回true表示确实没有人排在自己后面
return;
} else {
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
// 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
while (currentThread.next == null) { // step 5
}
}
}
currentThread.next.isWaiting = false;
currentThread.next = null;// for GC
}
}</code></pre><p>MCS 的能够保证较高的效率,降低不必要的性能消耗,并且它是公平的自旋锁。</p><h3>CLH 自旋锁</h3><p>CLH 锁与 MCS 锁的原理大致相同,都是各个线程轮询各自关注的变量,来避免多个线程对同一个变量的轮询,从而从 CPU 缓存一致性的角度上减少了系统的消耗。<br>CLH 锁的名字也与他们的发明人的名字相关:Craig,Landin and Hagersten。<br>CLH 锁与 MCS 锁最大的不同是,MCS 轮询的是当前队列节点的变量,而 CLH 轮询的是当前节点的前驱节点的变量,来判断前一个线程是否释放了锁。<br>实现如下所示:</p><pre><code class="java">import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private volatile boolean isWaiting = true; // 默认是在等待锁
}
private volatile CLHNode tail ;
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class, CLHNode .class , "tail" );
public void lock(CLHNode currentThread) {
CLHNode preNode = UPDATER.getAndSet( this, currentThread);
if(preNode != null) {//已有线程占用了锁,进入自旋
while(preNode.isWaiting ) {
}
}
}
public void unlock(CLHNode currentThread) {
// 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
if (!UPDATER .compareAndSet(this, currentThread, null)) {
// 还有后续线程
currentThread.isWaiting = false ;// 改变状态,让后续线程结束自旋
}
}
}</code></pre><p>从上面可以看到,MCS 和 CLH 相比,CLH 的代码比 MCS 要少得多;CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋;CLH的队列是隐式的,通过轮询关注上一个节点的某个变量,隐式地形成了链式的关系,但CLHNode并不实际持有下一个节点,MCS的队列是物理存在的,而 CLH 的队列是逻辑上存在的;此外,CLH 锁释放时只需要改变自己的属性,MCS 锁释放则需要改变后继节点的属性。</p><p>CLH 队列是 J.U.C 中 AQS 框架实现的核心原理。</p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=BF1lSkY4WK0q3Efg1iZO3g%3D%3D.evd7ko7RvaP6IKyx7DzwiHMOeIESz78N67IG54TZl81iVz9hbc%2FQl4BGBmkkwRlR" rel="nofollow">《深入理解 Java 虚拟机》第二版 - 周志明</a><br><a href="https://link.segmentfault.com/?enc=9bByVh4NcPjErKwW19CRRA%3D%3D.Hf0D6GBgY6c%2BDs%2BALkHzUJfQ9%2BKFsqNzLibi6SGW7GrzbwZg9XjSMdGAlg%2BjbcEL" rel="nofollow">自旋锁、排队自旋锁、MCS锁、CLH锁</a></p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
分布式事务概览
https://segmentfault.com/a/1190000016416574
2018-09-16T18:32:59+08:00
2018-09-16T18:32:59+08:00
ytbean
https://segmentfault.com/u/ytbean
3
<p>原文链接:<a href="https://link.segmentfault.com/?enc=b3rQe2T%2BLZucCxdTY9OOSg%3D%3D.RmNbqgczmu5l82fQenpKwNCeO%2FKwkC8AZ2xGWByMU5952ibsN2kJXUXcWigWKpSUMLZK%2FpwFkn%2FTq%2BfEyG031g%3D%3D" rel="nofollow">《分布式事务概览》http://www.ytbean.com/posts/distributed-tx-overview/</a></p><h2>传统的事务</h2><p>事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组SQL语句组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。</p><ul><li>原子性:一个事务是一个不可分割的工作单位,事务中包括的操作要么全部完成,要么全部取消</li><li>一致性:事务使数据库从一个一致性状态转换为另一个一致性状态,事务的中间状态不可被观察到。</li><li>隔离性:一个事务内部的操作及使用的数据对并发中的其它事务是隔离的。</li><li><p>持久性:一个事务一旦被提交,其对数据库中数据的改变是永久性的,不因其它故障而受影响。</p><h2>集中式数据库与分布式数据库</h2><h3>ACID</h3><p>以一个学生管理系统为例,可以看到对于这个学生成绩管理系统,不同的系统使用角色,对它有不同的要求:<br><img src="/img/remote/1460000041526408" alt="学生成绩管理系统.png" title="学生成绩管理系统.png"><br>那么传统的事务<br>ACID属性,是如何帮助我们做到满足这些需求的呢?<br><img src="/img/remote/1460000041526409" alt="ACID属性.png" title="ACID属性.png"></p></li></ul><h3>数据量变大</h3><p>而实际上,当我们考虑更大的数据量的时候,会发现,依赖于传统集中式数据库并无法很好地满足我们对系统的需求。当数据量变大,第一个要面对的问题是数据再也无法只放在一台机器上了,数据量的变大,意味着会有越来越多的客户端来查询这个数据库,那么就会出现查询的瓶颈,并且如果数据数非常重要的话,那么这些数据时丢不起的,而如果仅依赖于集中式数据库,那么当这个集中式数据库发生一些致命性的状况的时候,数据可能丢失。总而言之,对于集中式数据库来说,如果它出现了什么问题:不可查、丢数据等。那么其实意味着整个业务系统都陷入了不可用的状态。</p><p>总结下来,集中式数据库在通信、系统可靠性、可扩展性、性能瓶颈以及设计管理上,存在着明显的缺点:</p><ul><li>集中数据库会有多个成绩录入员,因为集中数据库只存在于某个服务器上,而成绩录入员却是分散在全国各地的,因此会造成额外的通信开销。</li><li>由于集中式数据库所有的数据都存在一个点上,那么一旦这个点发生故障,就会导致整个成绩管理系统停止运作,系统的可靠性差。</li><li>随着数据量的变大,录入的客户端变多,查询的客户端变多,那么存储系统本身的性能可能就会成为瓶颈,包括 CPU计算能力,IO吞吐能力,存储能力,都有可能成为瓶颈。</li><li>可扩展性差,正由于集中式数据库存在着性能差的问题,因此只能通过升级单机硬件能力的方式,实现数据库服务能力扩展,比如原来采用 MySQL 单机数据库,遇到访问瓶颈时更换磁盘,访问量更高时就需要考虑使用 Oracle 的商用解决方案、高端的存储设备、高端小型机,也就是 IOE 架构,甚至升级 IOE 设备,以换取更高的扩展和服务能力,这个过程就会存在设备升级和数据迁移的成本,其可扩展性的代价会面临巨大的成本问题。此外,根据摩尔定律,单机硬件能力的升级,并不能换来等比的效率加速比例,也就是说,增加多一倍的CPU核数,并不能带来一倍的性能提升,这个提升并不是线性的。</li><li>当一个系统的功能变得越来越复杂,例如说b不仅记录学生的成绩,还记录学生的奖惩历史,出勤情况,而数据库仍然只有一个点的情况下,集中数据库上承载的业务类型越来越多,导致管理困难。</li></ul><p>传统集中式数据库虽然能够很好地保证业务一致性,但其面临高速增长的访问量和数据量时存在性能和处理能力上的瓶颈。</p><h3>数据分布式存储</h3><p>分布式数据库虽然引进了复杂性例如分布式事务的问题,但是分布式数据库能解决集中式数据库的大多数痛点。<br>分布式数据库与集中式数据库的区别主要在数据分布和可扩展性两方面:</p><ul><li>分布式数据库的数据分散存储,集中式数据库的数据集中存储。</li><li>分布式数据库的扩展高效并且性价比高,而集中式数据库不能无限扩容并且扩容存在着成本导致的性价比的问题。</li></ul><p>总结起来,分布式数据库具有以下的特点:</p><ul><li>数据分布性,数据可以分布在不同的机器上,不同地理位置上。</li><li>数据统一性,虽然数据存放在不同的机器上,不同的地理位置上,但从整体上来看,它的系统逻辑应该是一致的</li><li>数据的透明性,虽然数据分散了,但是无论是查询还是更新,它们都应该有统一的入口</li><li>数据的安全性,单个数据节点如果出现错误,它不应该影响其它节点,从而数据库整体的安全性</li><li>数据的可扩展性,当现有集群称为瓶颈时,分布式数据库系统可以通过扩容来解决可扩展性的问题</li><li>数据的自治性,虽然数据分散存储,但每一个节点它都应该要能够独立管理自己的数据,同时又不影响整体的统一性。</li></ul><h2>分布式事务</h2><p>在高速增长的访问量和数据量的背景下,为了解决单机性能瓶颈以及可扩展性等问题,数据库分库分表拆分和服务化(微服务)的运用越来越广泛。完成一个业务功能,可能需要横跨多个服务或者横跨多个数据库节点;也就是说,需要操作的资源位于多个资源服务器上,从业务的角度来看,需要保证对多个资源服务器的操作,要么全部成功,要么全部失败。从本质上来说,分布式事务要保证不同资源服务器上的数据一致性。</p><h3>场景</h3><p>典型的分布式事务场景主要有跨库事务、分库分表以及跨服务事务。</p><h4>跨库事务</h4><p><img src="/img/remote/1460000041526410" alt="跨库事务场景.png" title="跨库事务场景.png"></p><h4>分库分表</h4><p>当对数据库通过中间件代理的形式进行水平拆分后,不可避免的会在一个事务中操作多个分片节点<br><img src="/img/remote/1460000041526411" alt="分库分表场景.png" title="分库分表场景.png"></p><h4>跨服务</h4><p>在服务化的架构下,完成业务功能可能涉及到对多个服务的调用,而这些服务分别会操作不同的数据库。需要保证跨服务对数据库的操作要么都成功,要么都失败,这是服务化场景下面临的分布式问题。<br><img src="/img/remote/1460000041526412" alt="跨服务场景.png" title="跨服务场景.png"></p><h2>X/Open DTP模型与XA规范</h2><h3>DTP 模型</h3><p>X/Open DTP(X/Open Distributed Transaction Processing Reference Model) 是X/Open 这个组织定义的一套分布式事务的标准,也就是了定义了规范和API接口,由厂商进行具体的实现。</p><h4>模型元素</h4><ul><li>应用程序(Application Program ,简称AP):用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作。</li><li>资源管理器(Resource Manager,简称RM):如数据库、文件系统等,并提供访问资源的方式。</li><li>事务管理器(Transaction Manager ,简称TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。</li><li>通信资源管理器(Communication Resource Manager,简称CRM):控制一个TM域(TM domain)内或者跨TM域的分布式应用之间的通信。</li><li>通信协议(Communication Protocol,简称CP):提供CRM提供的分布式应用节点之间的底层通信服务。</li></ul><p>一个DTP模型实例,至少有3个组成部分:AP、RMs、TM。如下所示:<br><img src="/img/remote/1460000041526413" alt="DTP模型.png" title="DTP模型.png"></p><p>AP通过TM来声明一个全局事务,然后操作不同的RM上的资源,最后通知TM来提交或者回滚全局事务。AP 可以和 TM 以及 RM 通信,TM 和 RM 互相之间可以通信,DTP模型里面定义了XA接口,TM 和 RM 通过XA接口进行双向通信,例如:TM通知RM提交事务或者回滚事务,RM把提交结果通知给TM。AP和RM之间则通过RM提供的Native API 进行资源控制,这个没有进行约API和规范,各个厂商自己实现自己的资源控制,比如Oracle自己的数据库驱动程序。</p><h3>XA 规范</h3><p>在DTP本地模型实例中,由AP、RMs和TM组成,不需要其他元素。AP、RM和TM之间,彼此都需要进行交互,如下图所示: </p><p><img src="/img/remote/1460000041526414" alt="交互.png" title="交互.png"></p><p>上图中(1)表示AP-RM的交互接口,(2)表示AP-TM的交互接口,(3)表示RM-TM的交互接口 <br>XA规范的最主要的作用是,就是定义了RM-TM的交互接口</p><p><img src="/img/remote/1460000041526415" alt="交互接口.png" title="交互接口.png"></p><p>XA规范中定义的RM 和 TM交互的接口如下图所示:</p><p><img src="/img/remote/1460000041526416" alt="交互接口" title="交互接口"></p><h4>二阶段提交</h4><p>XA规范除了定义的RM-TM交互的接口(XA Interface)之外,还对两阶段提交协议进行了优化。 两阶段协议(two-phase commit)是在OSI TP标准中提出的;在DTP参考模型中,指定了全局事务的提交要使用two-phase commit协议;而XA规范只是定义了两阶段提交协议中需要使用到的接口,也就是上述提到的RM-TM交互的接口,因为两阶段提交过程中的参与方,只有TM和RMs。 </p><p><img src="/img/remote/1460000041526417" alt="二阶段提交.png" title="二阶段提交.png"></p><ul><li>阶段1<br>TM通知各个RM准备提交它们的事务分支。如果RM判断自己进行的工作可以被提交,那就就对工作内容进行持久化,再给TM肯定答复;要是发生了其他情况,那给TM的都是否定答复。在发送了否定答复并回滚了已经的工作后,RM就可以丢弃这个事务分支信息。</li><li>阶段2<br>TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare失败的话,则TM通知所有RM回滚自己的事务分支。</li></ul><h4>二阶段提交优化</h4><p>XA规范对两阶段提交协议有2点优化:</p><ul><li>只读断言<br>在阶段1中,RM可以断言“我这边不涉及数据增删改”来答复TM的prepare请求,从而让这个RM脱离当前的全局事务,从而免去了Phase 2。 <br>这种优化发生在其他RM都完成prepare之前的话,使用了只读断言的RM早于AP其他动作(比如说这个RM返回那些只读数据给AP)前,就释放了相关数据的上下文(比如读锁之类的),这时候其他全局事务或者本地事务就有机会去改变这些数据,结果就是无法保障整个系统的可序列化特性,有脏读的风险。</li><li>一阶段提交<br>如果需要增删改的数据都在同一个RM上,TM可以使用一阶段提交跳过两阶段提交中的阶段1,直接执行阶段2。</li></ul><h4>二阶段提交缺点</h4><ol><li>同步阻塞<br>如果对操作读很敏感,我们需要将事务隔离级别设置为SERIALIZABLE。而对于分布式事务来说,更是如此,可重复读隔离级别不足以保证分布式事务一致性。</li><li>单点故障<br>一旦协调者TM发生故障。参与者RM会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)</li><li>数据不一致<br>在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。</li></ol><h4>三阶段提交</h4><p>由于二阶段提交存在着诸如同步阻塞、单点问题等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。 <br>三阶段提交(3PC),是二阶段提交(2PC)的改进版本,与两阶段提交不同的是,三阶段提交有两个改动点。</p><ol><li>引入超时机制,同时在协调者和参与者中都引入超时机制。</li><li>在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。<br> <img src="/img/remote/1460000041526418" alt="三阶段提交.png" title="三阶段提交.png"></li></ol><ul><li>CanCommit 阶段<br>3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。</li><li>PreCommit 阶段<br>协调者根据参与者的反应情况来决定是否可以继续事务的PreCommit操作。根据响应情况,有以下两种可能。 <br><strong>假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。</strong></li><li>发送预提交请求。协调者向参与者发送PreCommit请求,并进入Prepared阶段。</li><li>事务预提交。参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。</li><li>响应反馈。如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。</li></ul><p><strong>假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。</strong></p><ol><li>发送中断请求。协调者向所有参与者发送abort请求。</li><li>中断事务。参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。</li></ol><ul><li>doCommit阶段<br>该阶段进行真正的事务提交,也可以分为以下两种情况。<br><strong>情况1:执行提交</strong></li><li>发送提交请求。协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。</li><li>事务提交。参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。</li><li>响应反馈。事务提交完之后,向协调者发送Ack响应。</li><li><p>完成事务。协调者接收到所有参与者的ack响应之后,完成事务。</p></li></ul><p><strong>情况2:中断事务(协调者没有接收到参与者发送的ACK响应)</strong></p><ol><li>发送中断请求。协调者向所有参与者发送abort请求</li><li>事务回滚。参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。</li><li>反馈结果。参与者完成事务回滚之后,向协调者发送ACK消息</li><li><p>中断事务。协调者接收到参与者反馈的ACK消息之后,执行事务的中断。</p></li></ol><p>在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。<br>相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。<br>无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。</p><h2>CAP理论与BASE柔性事务</h2><h3>CAP 理论</h3><p>2000年7月,加州大学伯克利分校的 Eric Brewer 教授在分布式计算原则研究会议上提出 CAP 猜想。直到 2002 年,又麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP 理论,从而让 CAP 理论正式成为分布式计算领域的公认定理。<br>CAP理论:一个分布式系统最多只能同时满足一致性(consistency)、可用性(Availability)、分区容错性(partition-tolerance)这三项中的两项。<br>{% asset_img cap.jpg CAP理论模型 %}</p><ol><li>一致性<br>指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。</li><li>可用性<br>指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。</li><li>分区容错性<br>指分布式系统在遇到某节点或网络分区故障的时候,仍然能对外提供满足一致性和可用性的服务。</li></ol><h3>BASE 理论</h3><p>一个分布式系统无法同时满足一致性、可用性、分区容错性三个特点,需要对其进行取舍。对于一个分布式系统而言,分区容错性是一个最基本的要求,分布式系统中的组件必然需要被部署到不同的节点,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。<br>eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论。<br>BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。 <br>BASE是指Basically Available(基本可用)、Soft state(柔性状态)和Eventually consistent(最终一致性)</p><ol><li>基本可用<br> 指分布式系统在出现不可预知故障的时候,允许损失部分可用性。</li><li>柔性状态<br>指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。</li><li>最终一致<br>强调的是所有的数据更新操作,在经过一段时间的同步之后,最终都能够达到一个一致的状态。<br>BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲一致性获得高可用性。</li></ol><p>柔性事务解决方案一般有:最大努力通知、可靠消息最终一致性以及TCC。</p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=w2ZCoHQSygn3SK0NdSypKA%3D%3D.h23x8nnGMLKX1%2BAVro3RR8wzkPfrLGJQ1JL%2BMRg1Guq2H78yZWPRMz4pVo58cabkQBooIi%2FIl2UYRafvmSZ0UQ%3D%3D" rel="nofollow">Distributed Transaction Processing: Reference Model, Version 3</a><br><a href="https://link.segmentfault.com/?enc=iQ0MAkiB%2B7IBjD2qcM4E6g%3D%3D.YefJOCcpnplhCMFKRFP4trrKC41ef7f0D2N72jdj9oFh6gJ7pi4yi1Cihby6VUQtAyOywS7c0hH%2FHiC2pGBIbA%3D%3D" rel="nofollow">Distributed Transaction Processing:The XA Specification</a><br><a href="https://link.segmentfault.com/?enc=cVGiKxSp5raWsqCzfXiYtQ%3D%3D.38sKfKpRJh1uQbRijIHFcNJil9oh3krzEKLIhk2fFyVUzk8L8TVvnD8JKpTv7UR%2FtnF20JmnNxFTAW36edjaUw%3D%3D" rel="nofollow">Atomic Distributed Transactions: a RESTful Design</a><br><a href="https://link.segmentfault.com/?enc=mf9FGwHmbvbpqEFhs5xx0Q%3D%3D.dR9SN1fU2b2fB8w2UBp%2BeL05hIZy%2BDcNePx1Te%2FYeN7Y7nYK5iaXraoqzP7tsrqY8IXAtIEWT7BQ%2F21kW0hh6qDAnOAS4GHC%2FTF0SHByZ0E%3D" rel="nofollow">田守枝的技术博客</a></p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
JDBC 4.2 Specifications 中文翻译 -- 第十章 事务
https://segmentfault.com/a/1190000011164655
2017-09-14T12:00:06+08:00
2017-09-14T12:00:06+08:00
ytbean
https://segmentfault.com/u/ytbean
2
<p>事务用来提供数据集成性、正确的应用语义,以及并发访问时数据的一致性视图。所有符合 JDBC 规范的驱动都必须支持事务,JDBC 的事务管理 API 参照 SQL:2003 标准并且包含了以下的概念:</p>
<ul>
<li>自动提交模式</li>
<li>事务隔离级别</li>
<li>Savepoints</li>
</ul>
<p>本章讨论单个连接上的事务,涉及多条连接的事务将会在第十二章《分布式事务》中讨论。</p>
<h2>10.1 事务边界和自动提交</h2>
<p>什么时候应该开启一个事务,是 JDBC 驱动或者底层的数据源做的一个隐式的决定,尽管有一些数据源支持 begin transaction 语句,但这个语句没有对应的 JDBC API。当一条 SQL 语句要求开启一个事务并且当前没有事务未执行完,那么新事务就会被开启。<br>Connection 有一个属性 autocommit 来表明什么时候应该结束事务。如果 autocommit 启用,那么每一条 SQL 语句<strong>完全执行</strong>后,都会自动执行事务的提交。以下几种情况,视为<strong>完全执行</strong>:</p>
<ul>
<li>对于 DML 语句来说,例如 Insert,Update,Delete;以及 DDL 语句。这些语句在数据源端执行完毕就代表语句完全执行。</li>
<li>对于 Select 语句来说,完全执行意味着对应的结果集被关闭。</li>
<li>对于 CallableStatement 对象或者对于那些返回多个结果集的语句,完全执行意味着所有的结果集都关闭,以及所有的影响行数和出参都被获取到了。</li>
</ul>
<h3>10.1.1 关闭自动提交模式</h3>
<p>以下代码示范了如何关闭自动提交模式:</p>
<pre><code>// Assume con is a Connection object
con.setAutoCommit(false);</code></pre>
<p>当关闭自动提交,必须显式地调用 Connection 的 commit 方法提交事务或者调用 rollback 方法回滚事务。这种处理方式是合理的,因为事务的管理工作不是驱动应该做的,应用层应该自己管理事务,例如:</p>
<ul>
<li>当应用需要将一组 SQL 组成一个事务的时候</li>
<li>当应用服务器管理事务的时候</li>
</ul>
<p>autocommit 的默认值为 true,如果在一个事务的过程中,autocommit 的值被改变了,那么将会导致当前事务被提交。如果调用了 setAutocommit 方法,但没有改变原来的值,则不会产生其它附加影响,相当于没有调过一样。</p>
<p>如果一条连接参加了分布式事务,那 autocommit 不能设置为 true。第12章将会介绍到。</p>
<h2>10.2 事务隔离级别</h2>
<p>事务隔离级别定义了在一个事务中,哪些数据是对当前执行的语句“可见”的。在并发访问数据库时,事务隔离级别定义了多个事务之间对于同个目标数据源访问时的可交叉程度。可交叉程度可分为以下几类:</p>
<ul><li>dirty reads(脏读)</li></ul>
<p>当一个事务能看见另外一个事务未提交的数据时,就称为脏读,换言之,一个事务修改数据后再未提交之前,就能被其它事务看见,如果这个事务被回滚了而不是提交了,那么其它事务看到的数据则是不正确的,是“脏”的。</p>
<ul><li>nonrepeatable reads(前后不一致读)</li></ul>
<p>假设事务 A 读取了一行数据,接下来事务 B 改变了这行数据,之后事务 A 又再一次读取这行数据,这时候事务 A 就取到了两个不同的结果。</p>
<ul><li>phantom reads(幻读)</li></ul>
<p>假设事务 A 通过一个 where 条件读取到了一个结果集,事务 B 这时插入了一条符合事务 A 的 where 条件的数据,之后事务 A 通过同样的 where 条件再次进行查询时,发现了多出来一条数据。</p>
<p>JDBC 规范增加了 TRANSACTION_NONE 隔离级别,来满足了 SQL:2003 定义的 4 种事务隔离级别。隔离级别从最宽松到最严格,排序如下所示:</p>
<ul><li>TRANSACTION_NONE</li></ul>
<p>这意味着当前的 JDBC 驱动不支持事务,也意味着这个驱动不符合 JDBC 规范。</p>
<ul><li>TRANSACTION_READ_UNCOMMITTED</li></ul>
<p>允许事务看到其它事务修改了但未提交的数据,这意味着有可能是脏读、前后不一致读或者幻读。</p>
<ul><li>TRANSACTION_READ_COMMITTED</li></ul>
<p>一个事务在未提交之前,所做的修改不会被其它事务所看见。这能避免脏读,但避免不了前后不一致读和幻读。</p>
<ul><li>TRANSACTION_REPEATABLE_READ</li></ul>
<p>避免了脏读和前后不一致读,但幻读依然是有可能发生的</p>
<ul><li>TRANSACTION_SERIALIZABLE</li></ul>
<p>避免了脏读、前后不一致读以及幻读</p>
<h3>10.2.1 使用 setTransactionIsolation 方法</h3>
<p>一条连接的默认事务隔离级别是由驱动决定的,这个隔离级别也往往是底层的数据源默认的事务隔离级别。</p>
<p>应用程序可以使用 Connection 类里的 setTransactionIsolation 方法来改变一条连接的事务隔离级别。如果在一个事务的过程中调用 setTransactionIsolation 方法,会有什么样结果,完全由驱动的实现决定。</p>
<p>getTransactionIsolation 方法的返回值应当能正确地反映当前连接的事务隔离级别,建议实现驱动的时候要实现 setTransactionIsolation 方法,可以在一个事务开启之前去设置事务隔离级别。此外,调用 <br>setTransactionIsolation 这个方法时,自动提交当前事务,也是一种合理的 setTransactionIsolation 实现。</p>
<p>可能有些驱动实现并不支持所有的四种事务隔离级别,如果通过 setTransactionIsolation 方法设置的隔离级别驱动不支持的话,驱动可以主动将事务隔离级别设置为更高更严格的事务隔离级别,如果没法设置为更高或者更严格的,驱动应该抛出 SQLException。可以使用 DatabaseMetaData 的 supportsTransactionIsolationLevel 方法来判断驱动是否支持某个事务隔离级别。</p>
<h3>10.2.2 性能考虑</h3>
<p>事务隔离级别设置得越高,为了保证事务的正确语义,意味着会有更多的锁等待、锁竞争以及 DBMS 的附加损耗。这反过来也会降低并发访问性,所以应用程序可能会发现事务隔离级别越高时,性能反而会下降。为此,事务的管理者应该权衡两者的利弊,设置合理的事务隔离级别。</p>
<h2>10.3 Savepoints</h2>
<p>savepoints 可以在一个事务的中间设置一个标记点,来更灵活地控制事务。一旦事务设置了一个标记点,事务可以回滚到这个标记点,不会影响标记点之前的操作。可以使用 DatabaseMetaData.supportsSavepoints 方法来判断驱动或者数据库是否支持这个功能。</p>
<h3>10.3.1 设置并回滚到标记点</h3>
<p>Connection.setSavepoint 方法可以用来在当前事务中设置一个标记点,同时如果当前没有在事务中,调用这个方法能开启一个事务。 Connection.rollback 方法有一个重载版本,能够接收一个 savepoint 作为参数。</p>
<pre><code>conn.createStatement();
int rows = stmt.executeUpdate("INSERT INTO TAB1 (COL1) VALUES " +
"(’FIRST’)");
// set savepoint
Savepoint svpt1 = conn.setSavepoint("SAVEPOINT_1");
rows = stmt.executeUpdate("INSERT INTO TAB1 (COL1) " +
"VALUES (’SECOND’)");
...
conn.rollback(svpt1);
...
conn.commit();</code></pre>
<p>上面的代码实例中,插入一行数据后,保存一个标记点,然后插入一行数据。当事务被回滚到标记点的时候,第二行数据不会被插入,第一行数据依然会被插入。当连接提交后,第一行数据将会保存在表里。</p>
<h3>10.3.2 释放标记点</h3>
<p>Connection.releaseSavepoint 方法接收一个 Savepoint 作为参数,删除这个标点以及在它之后的标记点。如果一个 savepoint 已经被释放了,还把它作为 rollback 的参数的话,将会导致 SQLException。当事务提交或者完全回滚的时候,所有的 savepoints 都会被自动释放。当回滚到某个 savepoint 后,这个 savepoint 以及在它之后定义的 savepoint 都会被自动释放掉。</p>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
JDBC 4.2 Specifications 中文翻译 -- 第九章 连接
https://segmentfault.com/a/1190000011059038
2017-09-07T15:22:52+08:00
2017-09-07T15:22:52+08:00
ytbean
https://segmentfault.com/u/ytbean
1
<p>一个 <code>Connection</code> 对象,表示了与某个数据源的一条连接,数据源的种类可以是关系型数据库,文件系统等等之类,只要有对应的 JDBC 驱动,都可以称之为数据源。应用程序使用 JDBC API 来维护多条连接,这些连接可能访问的是多个数据源,也可能访问的只是一个数据源。从 JDBC 驱动的角度来看,一个 <code>Connection</code> 对象就意味着一个客户端会话,一个会话会保持许多状态,例如用户 ID,一系列的 SQL Statement 以及结果集,也保存了当前使用的事务处理策略。</p>
<p>可以通过以下两种方式之一来获取一条连接:</p>
<ul>
<li>使用 <code>DriverManager</code> 这个类以及各种各样的驱动实现</li>
<li>使用 <code>DataSource</code> 类</li>
</ul>
<p>更推荐使用 <code>DataSource</code> 对象来获取连接,因为这增强了应用的可移植性,使得代码更容易维护了,并且使得对连接池和分布式事务的使用更加地透明。所有的 Java EE 组件,都会使用 DataSource 对象来获取连接。</p>
<p>这一章将会介绍各种不同的 JDBC 驱动以及如何使用 <code>Driver</code> 接口、<code>DriverManager</code> 类以及基本的 <code>DataSource</code> 接口。关于连接池和分布式事务的介绍分别在第11章和第12章做介绍。</p>
<h2>9.1 驱动的种类</h2>
<ul><li>Type 1</li></ul>
<p>这种类型的 JDBC 驱动是对另外一种访问 API 的映射,比如说 ODBC,一般需要依赖本地库,这就导致了它的可移植性不行。JDBC-ODBC 桥就是这种类型的驱动。</p>
<ul><li>Type 2</li></ul>
<p>这种类型的 JDBC 驱动一部分是用 Java 语言写的,一部分是用本地代码写的。这种驱动使用一个本地的客户端库来连接数据源。由于对本地代码的使用,可移植性也不行。</p>
<ul><li>Type 3</li></ul>
<p>这种类型的驱动使用纯 Java 语言编写,但是通信的时候需要经过一个中间件,使用的是与数据库具体协议无关的独立协议。这个中间件转发客户端的请求给后面的数据源。</p>
<ul><li>Type 4</li></ul>
<p>这种类型的驱动使用纯 Java 语言编写,并且使用网络协议或者文件 IO 与具体的数据源通信,客户端直接与数据源连接。</p>
<h2>9.2 Driver 接口</h2>
<p>编写 JDBC 驱动,必须实现 Driver 接口,并且实现类中必须包含一个静态初始化块,当驱动被加载时,这块代码会被调用。这块代码的主要工作是讲自己注册给 DriverManager,如下代码所示:</p>
<pre><code>public class AcmeJdbcDriver implements java.sql.Driver {
static {
java.sql.DriverManager.registerDriver(new
AcmeJdbcDriver());
}
}</code></pre>
<p>驱动的实现类必须提供一个无参构造函数,当 DriverManager 想要与 Driver 交互时,它会直接调用它的方法,Driver 接口包含了一个 acceptsURL 方法,DriverManager 可以调用这个方法来判断该驱动是否能处理对应的 JDBC URL。</p>
<p>当 DriverManager 想要建立一条数据库连接时,它会调用驱动实现类的 connect 方法,并把 URL 作为参数穿给它,这个方法会返回一个 Connection 对象,或者是当无法建立数据库连接时,抛出一个 SQLException。如果驱动实现类无法解析 URL,这个方法将会返回 null。</p>
<p>## 9.2.1 加载一个实现了 java.sql.Driver 接口的驱动类<br>DriverManager 初始化的时候,会先通过 “java.drivers” 这个系统属性来尝试加载驱动,如以下例子:</p>
<pre><code>java -Djdbc.drivers=com.acme.jdbc.AcmeJdbcDriver Test</code></pre>
<p>DriverManager 的 getConnection 方法能够支持 Java SE 的 SPI 服务发现机制,JDBC 4.0 的驱动必须包含以下文件 “META-INF/services/java.sql.Driver”,这个文件会包含实现了 Driver 接口的类名</p>
<h2>9.3 DriverAction 接口</h2>
<p>当 DriverManager 的 deregisterDriver 方法被调用时,如果想要被通知到,那么 JDBC 驱动就得有对应的实现了 DriverAction 接口的类,DriverAction 的具体实现类并不希望直接被上层应用拿来使用,所以实现 JDBC 驱动的时候,应该将这个类定义为私有的类,以防止被直接使用。</p>
<p>JDBC 驱动的静态初始化块里面,必须调用 DriverManager.registerDriver(java.sql.Driver, java.sql.DriverAction) 方法,这样当一个 JDBC 驱动被 DriverManager 注销的时候,才能被通知到,如下所示:</p>
<pre><code>public class AcmeJdbcDriver implements java.sql.Driver {
static DriverAction da;
static {
java.sql.DriverManager.registerDriver(new
AcmeJdbcDriver(), da);
}
}</code></pre>
<h2>9.4 DriverManager 类</h2>
<p>DriverManager 类与 Driver 接口一起协作,维护所有可用的 JDBC 驱动。当应用程序通过一个 URL 来获取一个连接的时候, DriverManager 负责找到一个适用该 URL 的驱动,用这个驱动来获取对应的数据源的连接。<br>DriverManager 的关键方法如下所示:</p>
<ul><li>registerDriver</li></ul>
<p>这个方法会将某个驱动加进可用驱动的集合里,它在一个驱动被装载的时候隐式地调用,一般情况下是驱动的静态代码块里调用这个方法。</p>
<ul><li>getConnection</li></ul>
<p>这个方法用来获取一个连接,要调用这个方法,必须提供一个 JDBC URL,DriverManager 会使用这个 URL 来轮询所有已经注册的驱动,并找到一个可以识别这个 URL 的驱动,驱动会返回一个 Connection 给 DriverManager,然后再把它交给应用程序。</p>
<p>JDBC URL 的格式如下所示:</p>
<pre><code>jdbc:<subprotocol>:<subname></code></pre>
<p>subprotocol 定义是要连接的是哪种类型的数据库,subname 则会根据 subprotoco 的不同而不同。<br>以下代码示范了如何从 DriverManager 获取一个连接:</p>
<pre><code>String url = "jdbc:derby:sample";
String user = "SomeUser";
String passwd = "SomePwd";
Connection con = DriverManager.getConnection(url, user, passwd);</code></pre>
<p>DriverManager 类也提供了另外一些获取连接的方法:</p>
<ul><li>getConnection(String url)</li></ul>
<p>这个方法适用于不需要提供用户名和密码的情况</p>
<ul><li>getConnection(String url, java.util.Properties prop)</li></ul>
<p>这个方法允许在 prop 参数里加入用户名和密码,以及其它属性</p>
<p>DriverPropertyInfo 这个类提供了一个驱动可以理解的所有的属性,详见 Java JDBC API DOC</p>
<h2>9.5 SQLPermission 类</h2>
<p>这个类代表了一个代码基所拥有的权限。当前唯一定义的权限是 setLog 权限。当一个 Applet 调用了 DriverManager 的 setLogWriter 或者 setLogStream 方法时,SecurityManager 将会检查是否有权限。如果没有权限,将会抛出一个 java.lang.SecurityException 异常</p>
<h2>9.6 DataSource 接口</h2>
<p>DataSource 这个接口是在 JDBC2.0 的可选属性里引进的,这是 JDBC 规范推荐的用来获取数据源连接的方式。实现了 DataSource 接口的 JDBC 驱动会返回和通过 DriverManager 获取的相同的 Connection 实例,使用 DataSource 接口使应用程序更加具有可移植性,因为应用程序不需要为某个特定的驱动提供相关的连接信息,仅仅需要提供一个逻辑的数据源名。逻辑数据源名用来映射到 JNDI 提供的 DataSource 实例。这个 DataSource 实例代表了一个物理上的数据源,并提供获取相应连接的方法。如果关于数据源的属性或者信息发生了变化,DataSource 对象可以感知到对应的变化,完全不需要改变应用代码。<br>实现 DataSource 接口时,应该透明地提供以下功能:</p>
<ul>
<li>通过连接的池化来提高性能和可扩展性</li>
<li>通过 XADataSource 接口来支持分布式事务</li>
</ul>
<p>还需要注意的是,DataSource 的实现类必须提供一个无参构造函数<br>接下来的3个小节主要讨论:</p>
<ol>
<li>基本的 DataSource 属性</li>
<li>使用 JNDI API 如何提供应用的可移植性以及可维护性</li>
<li>如果获取一个连接</li>
</ol>
<h3>9.6.1 DataSource 的属性</h3>
<p>JDBC API 定义了一系列的属性来描述 DataSource 的实现,具体的属性有哪些,取决于具体的 DataSource 实现,也就是说,取决于该实现是一个基本的 DataSource 对象,还是 ConnectionPoolDataSource,或者是 XADataSource,无论什么实现,它们都会有共同的属性 description,以下是标准的 DataSource 属性:</p>
<table>
<thead><tr>
<th>属性名</th>
<th>数据类型</th>
<th>描述</th>
</tr></thead>
<tbody>
<tr>
<td>databaseName</td>
<td>String</td>
<td>数据库名</td>
</tr>
<tr>
<td>dataSourceName</td>
<td>String</td>
<td>数据源名,用来命名底层的 XADataSource 或者是 ConnectionPoolDataSource</td>
</tr>
<tr>
<td>description</td>
<td>String</td>
<td>对此 DataSource 的描述信息</td>
</tr>
<tr>
<td>networkProtocol</td>
<td>String</td>
<td>网络协议</td>
</tr>
<tr>
<td>password</td>
<td>String</td>
<td>数据库密码</td>
</tr>
<tr>
<td>portNumber</td>
<td>int</td>
<td>数据库监听端口</td>
</tr>
<tr>
<td>roleName</td>
<td>String</td>
<td>初始 SQL roleName</td>
</tr>
<tr>
<td>serverName</td>
<td>String</td>
<td>数据库服务器名</td>
</tr>
<tr>
<td>user</td>
<td>String</td>
<td>数据库用户名</td>
</tr>
</tbody>
</table>
<p>DataSource 的属性遵循 JavaBean 1.01 规范。具体 DataSource 实现可以添加属性,但是不能与原有的有冲突。这些属性必须提供对应的 setter 和 getter 方法,当一个新的 DataSource 初始化的时候,这些属性也应该相应进行初始化,如以下代码所示,这里的实现是一个 VendorDataSource:</p>
<pre><code>VendorDataSource vds = new VendorDataSource();
vds.setServerName("my_database_server");
String name = vds.getServerName();</code></pre>
<p>DataSource 的属性,设计的初衷是不应该直接被应用代码获取,应该在具体的实现类里提供获取的方法,而不是在 DataSource 上定义 public 的属性,想要获取属性值,可以通过“自省”的方式(反射)来获取。</p>
<h3>9.6.2 JNDI API 以及应用可移植性</h3>
<p>Java Naming and Directory Interface (JNDI) API 提供让应用通过网络访问远程服务的统一方式,本小节将描述如何使用 JNDI 来注册并访问一个 JDBC 数据源对象。更详细的信息可以查阅 JNDI 规范。<br>使用 JNDI API,应用可以通过指定一个逻辑名来访问一个数据源,在这里 JNDI 需要使用到命名服务,来将逻辑名映射到对应的数据源。这个特性极大地增强了应用的可移植性,因为很多数据源的配置,可以在不修改应用层代码的情况下进行修改,例如端口号和服务器名。事实上,应用可以透明地访问另一个完全不同的数据源,只需要修改对应的配置。在三层架构的环境中,这个特性很重要,应用服务器会将访问不同数据源的细节隐藏起来,不需要对应用开放。</p>
<p>以下代码实例了如何使用 JNDI 来注册一个数据源对象:</p>
<pre><code>// Create a VendorDataSource object and set some properties
VendorDataSource vds = new VendorDataSource();
vds.setServerName("my_database_server");
vds.setDatabaseName("my_database");
vds.setDescription("data source for inventory and personnel");
// Use the JNDI API to register the new VendorDataSource object.
// Reference the root JNDI naming context and then bind the
// logical name "jdbc/AcmeDB" to the new VendorDataSource object.
Context ctx = new InitialContext();
ctx.bind("jdbc/AcmeDB", vds);</code></pre>
<h3>9.6.3 通过 DataSource 实例获取连接</h3>
<p>一旦一个 DataSource 注册在 JNDI 的命名服务后,应用可以使用它来获取一条到物理数据源的连接,如下代码所示:</p>
<pre><code>// Get the initial JNDI naming context
Context ctx = new InitialContext();
// Get the DataSource object associated with the logical name
// "jdbc/AcmeDB" and use it to obtain a database connection
DataSource ds = (DataSource)ctx.lookup("jdbc/AcmeDB");
Connection con = ds.getConnection("user", "pwd");</code></pre>
<h3>9.6.4 关闭连接</h3>
<p>Connection.close(), Connection.isclosed() 和 Connection.isValid() 这些方法可以用来关闭一条连接和判断一条连接是否还处于活跃状态。</p>
<h4>9.6.4.1 Connection.close</h4>
<p>An application calls the method当应用使用完一条连接后,可以调用 Connection.close() 来关闭这条连接,在这条连接上所有的 Statement 对象也会被关闭。<br>一条连接关闭后,除了 close(), isClosed() 和 isValid() 方法外,调用其它的方法将会抛出一个 SQLException。</p>
<h4>9.6.4.2 Connection.isClosed</h4>
<p>这个方法用来判断一条连接的 close() 方法是否已经被调用过,这个方法不能用来判断连接是否还可用。<br>但是有写 JDBC 驱动可能会增强 isClosed() 方法,使得可以利用这个方法来判断一条连接是否还可用。在这里,为了最大的可移植性,应用应该通过 Connection.isValid() 来判断一条连接是否还可用。</p>
<h4>9.6.4.3 Connection.isValid</h4>
<p>这个方法用来标识一条连接是否还可用,如果不可用,那么除了 close(),isClosed() 和isValid() 方法之外,调用其它方法将会抛出 SQLException</p>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
JDBC 4.2 Specifications 中文翻译 -- 第八章 异常
https://segmentfault.com/a/1190000010615542
2017-08-11T15:27:04+08:00
2017-08-11T15:27:04+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>当访问一个数据源时发生错误或者警告,JDBC 用 <code>SQLException</code> 这个类及其子类来表示并提供相关的异常信息。</p>
<h2>8.1 SQLException</h2>
<p>SQLException 由一下几部分组成:</p>
<ul>
<li>描述错误的文本信息。可以通过 <code>SQLException.getMessage()</code> 来获取。</li>
<li>一个 SQLState 对象。可以通过 <code>SQLException.getSQLStateType()</code> 来获取。</li>
<li>错误码,是某种错误类型的一个编码,int 类型,可以通过 <code>SQLException.getErrorCode()</code> 来获取。</li>
<li>底层的异常,是一个 <code>Throwable</code> 对象,用来代表引起 <code>SQLException</code> 发生的真正原因,通过 <code>SQLException.getCause()</code> 来获取。</li>
<li>异常链的引用,如果有不止一个 SQLException 异常,可以通过递归式地调用 <code>SQLException.getNextException</code> 来获取整个异常链,直到这个方法返回 null,异常链结束。</li>
</ul>
<h3>8.1.1 对 Java SE 异常链的支持</h3>
<p>SQLException 和它的子类都支持 Java SE 的异常链机制,为了实现这个功能,有以下几个地方特意做了处理:</p>
<ul>
<li>增加额外的四个构造函数</li>
<li>支持对异常链的 For-Each 语法</li>
<li>getCause 方法不一定会返回 SQLException 类型或者其子类型</li>
</ul>
<p>可以参考 JDBC API Java DOC 获取更详细的信息</p>
<h3>8.1.2 遍历多个 SQLException</h3>
<p>在一次 SQL 语句的执行中,很有可能会发生一个或者多个异常,多个异常之间有着相应的联系,这意味着,当捕获到一个 SQLException 时,它的背后可能还有多个 SQLException 链接在它身上,为了遍历这条异常链,通常可以通过循环调用 <code>SQLException.getNextException</code> 方法,直到该方法返回 Null。<br>同时,也可以通过循环调用 <code>SQLException.getCause()</code> 来获得引起一个 SQLException 的真正原因,直到该方法返回 Null。<br>以下代码示范了如何获取所有的 SQLException 以及其原因:</p>
<pre><code class="java">catch(SQLException ex) {
while(ex != null) {
System.out.println("SQLState:" + ex.getSQLState());
System.out.println("Error Code:" + ex.getErrorCode());
System.out.println("Message:" + ex.getMessage());
Throwable t = ex.getCause();
while(t != null) {
System.out.println("Cause:" + t);
t = t.getCause();
}
ex = ex.getNextException();
}
}</code></pre>
<h4>8.1.2.1 使用 For-Each 循环遍历 SQLException</h4>
<p>以下代码示范了如何使用:</p>
<pre><code>catch(SQLException ex) {
for(Throwable e : ex ) {
System.out.println("Error encountered: " + e);
}
}</code></pre>
<h2>8.2 SQLWarning</h2>
<p><code>SQLWarning</code> 是 <code>SQLException</code> 的子类, 下面这些接口的方法都有可能在对数据库进行操作的时候产生 SQLWarning</p>
<ul>
<li>Connection</li>
<li>DataSet</li>
<li>Statement</li>
<li>ResultSet</li>
</ul>
<p>当使用者调用其中一个方法后,如果产生了 SQLWarning,使用者并不会马上就被通知到,而必须使用者主动地去调用 <code>getWarnings</code> 方法才能获得。SQLWarning 也可以支持链式的机制,通过<code>SQLWarning.getNextWarning</code> 方法可以获取整个链条。</p>
<h2>8.3 DataTruncation</h2>
<p><code>DataTruncation</code> 类是 <code>SQLWarning</code> 的一个子类,用来表示当数据被裁截时的警告。如果向数据库写入数据时发生了数据裁截,<code>DataTruncation</code> 会像异常一样被抛出来通知调用者,如果是在读取数据库数据的时候发生裁截,那么只会产生一个警告。<br>一个 <code>DataTruncation</code> 对象由以下部分组成:</p>
<ul>
<li>描述文本</li>
<li>一个代码为 01004 的 SQLState(当读取数据时发生数据裁截)</li>
<li>一个代码为 22001 的 SQLState(当写入数据的时候发生数据裁截)</li>
<li>一个 Boolean 类型的标识,标识是字段值被裁剪还是参数被裁剪,DataTruncation.getParameter 方法如果返回 true 则是参数被裁截,否则是字段值被裁截</li>
<li>一个 int 值代表被裁截的字段或者参数的下标值</li>
<li>一个 Boolean 类型的标识,用来标识是读取还是写入的时候发生的数据裁截,通过 <code>DataTruncation.getRead </code> 来判断</li>
<li>
<code>DataTruncation.getDataSize</code> 方法返回一个 int 值,用来代表被裁截前的数据大小,如果返回 -1 代表大小未知</li>
<li>
<code>DataTruncation.getTransferSize</code> 方法返回一个 int 值,代表裁截后的实际数据大小</li>
</ul>
<h3>8.3.1 静默裁截</h3>
<p><code>Statement.setMaxFieldSize</code> 方法允许设置一个最大的字段长度限制,这个限制只对这些数据类型有效:BINARY, VARBINARY, LONGVARBINARY, CHAR, VARCHAR, LONGVARCHAR, NCHAR, NVARCHAR 和 LONGNVARCHAR。如果设置了这个限制,并且从数据库读出超过这个限制的长度的字段值时,将不会抛出 DataTruncation</p>
<h2>8.4 BatchUpdateException</h2>
<p>当批量的 SQL 语句被执行时,有可能会抛出 <code>BatchUpdateException</code>,具体见第十四章。</p>
<h2>8.5 可分类的 SQLExceptions</h2>
<p>SQLExceptions 可分为以下三种类型:</p>
<ul>
<li>SQLNonTransientException</li>
<li>SQLTransientException</li>
<li>SQLRecoverableException</li>
</ul>
<h3>8.5.1 NonTransient SQLExceptions</h3>
<p>一个 NonTransient SQLExceptions 必须继承自 SQLNonTransientException 类,如果一次数据库操作发生错误,在这个错误的原因未解决之前,继续调用相同的数据库操作,将会抛出一个 NonTransient SQLException,如果抛出的异常不是 SQLNonTransientConnectionException,那么还可以认为数据库连接依然是有效的,一下列表定义了具体的 SQLNonTransientSQLException 与 SQLState 的对应关系:</p>
<table>
<thead><tr>
<th>SQL State</th>
<th>SQLNonTransientSQLException 子类</th>
</tr></thead>
<tbody>
<tr>
<td>0A</td>
<td>SQLFeatureNotSupportedException</td>
</tr>
<tr>
<td>08</td>
<td>SQLNonTransientConnectionException</td>
</tr>
<tr>
<td>22</td>
<td>SQLDataException</td>
</tr>
<tr>
<td>23</td>
<td>SQLIntegrityConstraintViolationException</td>
</tr>
<tr>
<td>28</td>
<td>SQLInvalidAuthorizationExceptio</td>
</tr>
<tr>
<td>42</td>
<td>SQLSyntaxErrorException</td>
</tr>
</tbody>
</table>
<p>除了上述表格提到的,具体的数据库驱动实现也可以增加自己的 NonTransient Exception 类型</p>
<h3>8.5.2 Transient Exceptions</h3>
<p>Transient Exception 与 NonTransient Exception 的不同是,如果一次数据库操作发生了错误,那么在连接还有效的情况下,可以假设第二次相同的操作有可能成功。一个 Transient Exception 必须是 SQLTransientException 的子类。与 SQL State 的对应关系如下所示:</p>
<table>
<thead><tr>
<th>SQL State</th>
<th>SQLTransientSQLException 子类</th>
</tr></thead>
<tbody>
<tr>
<td>08</td>
<td>SQLTransientConnectionException</td>
</tr>
<tr>
<td>40</td>
<td>SQLTransactionRollbackException</td>
</tr>
<tr>
<td>N/A</td>
<td>SQLTimeoutException</td>
</tr>
</tbody>
</table>
<p>除了上述表格提到的,具体的数据库驱动实现也可以增加自己的 Transient Exception 类型</p>
<h3>8.5.3 SQLRecoverableException</h3>
<p>这个异常代表可恢复异常,所谓的可恢复是指,如果一次数据库操作失败了,那么应用层可以通过某些步骤来进行恢复设置,并且重新做同样的数据库操作后能成功。例如说,弃用旧连接,重新获取一条数据库连接。如果发生了 SQLRecoverableException,应用必须假设当前的连接已经是不可用的。</p>
<h2>8.6 SQLClientinfoException</h2>
<p>当调用 <code>Connection.setClientInfo</code> 方法来设置客户端信息时,可能会抛出这个异常。</p>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
JDBC 4.2 Specifications 中文翻译 -- 第七章 数据库元数据
https://segmentfault.com/a/1190000010613488
2017-08-11T13:49:08+08:00
2017-08-11T13:49:08+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>JDBC 驱动需要实现 <code>DatabaseMetaData</code> 这个接口,以便向驱动的使用者提供一些关于底层数据源的信息。这个接口主要被应用服务器以及一些工具型代码使用,以决定如何与一个数据源交互,应用程序有时候也会使用这个接口里的方法去获得数据源的信息,但这种用法并不是典型的用法。</p>
<p><code>DatabaseMetaData</code> 这个接口拥有超过 150 个方法,这么多的方法,可以根据它们提供的信息的类型进行分类,信息类型有:</p>
<ul>
<li>关于数据源通用的信息</li>
<li>关于数据源是否支持某个特性或者是否具有某种能力的信息</li>
<li>数据源的限制</li>
<li>数据源有哪些 SQL 对象,以及这些 SQL 对象都拥有哪些属性</li>
<li>数据源是否支持事务</li>
</ul>
<p><code>DatabaseMetaData</code> 接口也拥有超过 40 个字段,当调用接口的方法时,这些字段常量会作为返回值。</p>
<p>本章会对 <code>DatabaseMetaData</code> 做一个概览,给出一些实例来验证接口定义的方法,并介绍一些新的方法。如果希望更深入地理解所有的方法,建议读者参考 JDBC API Specification。</p>
<blockquote>注意,JDBC 也定义了 <code>ResultSetMetaData</code> 接口,这个接口我们将会在第十五章遇见它。</blockquote>
<h2>7.1 创建一个 DatabaseMetaData 对象</h2>
<p>通过 <code>Connection</code> 接口的 <code>getMetaData</code> 方法来创建一个 <code>DatabaseMetaData </code> 对象。一旦这个对象创建完成,就可以使用这个对象动态地查询与底层数据源有关的信息。以下代码示例创建了一个 <code>DatabaseMetaData</code> 对象,并使用这个对象来获取底层数据源支持的表名最大字符数是多少</p>
<pre><code>// 在这里 con 是一个 Connection 对象
DatabaseMetaData dbmd = con.getMetadata();
int maxLen = dbmd.getMaxTableNameLength();</code></pre>
<h2>7.2 获取通用的信息</h2>
<p>有一些 <code>DatabaseMetaData</code> 接口的方法用来动态地获取底层数据源的一些通用的信息和实现细节。例如以下这些方法:</p>
<ul>
<li>getURL</li>
<li>getUserName</li>
<li>getDatabaseProductVersion, getDriverMajorVersion 和</li>
</ul>
<p>getDriverMinorVersion</p>
<ul>
<li>getSchemaTerm, getCatalogTerm 和 getProcedureTerm</li>
<li>nullsAreSortedHigh 和 nullsAreSortedLow</li>
<li>usesLocalFiles 和 usesLocalFilePerTable</li>
<li>getSQLKeyword</li>
</ul>
<h2>7.3 确认数据源是否支持某些特性</h2>
<p><code>DatabaseMetaData</code> 有一大堆方法可以用来确定驱动或者底层数据源是否支持某个特性或特性集合。不仅如此,有一些方法还描述了对该特性支持到了哪个层次。<br>能用来确认是否支持某个特性的方法有:</p>
<ul>
<li>supportsAlterTableWithDropColumn</li>
<li>supportsBatchUpdates</li>
<li>supportsTableCorrelationNames</li>
<li>supportsPositionedDelete</li>
<li>supportsFullOuterJoins</li>
<li>supportsStoredProcedures</li>
<li>supportsMixedCaseQuotedIdentifiers</li>
</ul>
<p>能用来确认对某个特性的支持层次的方法有:</p>
<ul>
<li>supportsANSI92EntryLevelSQL</li>
<li>supportsCoreSQLGrammar</li>
</ul>
<h2>7.4 数据源限制</h2>
<p><code>DatabaseMetaData</code> 有另外一组方法可以用来获取指定数据源在某些方面的限制,其中一些方法如下所示:</p>
<ul>
<li>getMaxRowSize</li>
<li>getMaxStatementLength</li>
<li>getMaxTablesInSelect</li>
<li>getMaxConnections</li>
<li>getMaxCharLiteralLength</li>
<li>getMaxColumnsInTable</li>
</ul>
<p>这一类方法的返回值都是一个 int 类型的数字,如果返回为0则代表该项资源<strong>没有限制或者是不确定的</strong>。</p>
<h2>7.5 SQL 对象以及属性</h2>
<p><code>DatabaseMetaData</code> 类有一些方法,可以提供给我们那些组成了一个具体数据源的 SQL 对象的信息,也相应地提供了可以获取这些 SQL 对象的属性的方法,这些方法都返回一个 ResultSet 作为结果,其中的一行代表一个 SQL 对象,例如,方法 <code>getUDTs()</code> 会返回一个 ResultSet 对象作为结果,其中的每一行都代表一个<em>用户自定义类型</em>。常见的方法如以下:</p>
<ul>
<li>getSchemas</li>
<li>getCatalogs</li>
<li>getTables</li>
<li>getPrimaryKeys</li>
<li>getProcedures</li>
<li>getProcedureColumns</li>
<li>getUDTs</li>
<li>getFunctions</li>
<li>getFunctionColumns</li>
</ul>
<p>这一类的方法返回的 ResultSet 对象,是 <strong>TYPE_FORWARD_ONLY</strong> 也是 <strong>CONCUR_READ_ONLY</strong> 的,这代表这个 ResultSet 的指针只能向后滚动,不能向前滚动,并且对结果集的更新不会同步到数据库里。<code>ResultSet.getHoldability</code> 方法可以用来获取 ResultSet 的可保存性的默认值,这个默认值不同的驱动实现都有可能不同。</p>
<p>如果 ResultSet 返回的结果有不同的厂商自定义的一些额外的字段,那么在实现驱动的时候,取出这个字段的值时必须使用字段的名称作为参数来取,因为 JDBC 规范可能以后也会对 DatabaseMetaData 这个类的方法返回的结果集增加字段值,使用字段名来取值可以使得 JDBC 规范的更新或改动不会对旧版的驱动实现造成影响。</p>
<h2>7.6 事务支持</h2>
<p><code>DatabaseMetaData</code> 提供了少量的方法,用来查看底层的数据源是否支持某些事务特性,例如:</p>
<ul>
<li>supportsMultipleTransactions</li>
<li>getDefaultTransactionIsolation</li>
</ul>
<h2>7.7 新增的方法</h2>
<p>jdbc 4.2 规范为 <code>DatabaseMetaData</code> 引进了一些新的方法:</p>
<ul>
<li>supportsRefCursors</li>
<li>getMaxLogicalLobSize</li>
</ul>
<p>这两个方法的描述信息在 JDBC API 的 JavaDoc 里有更详细的说明</p>
<h2>7.8 修改的方法</h2>
<ul><li>getIndexInfo 返回的 CARDINALITY 和 PAGES 字段的类型现在变成了 long</li></ul>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
netty4.x ByteBuf 基本机制及其骨架实现
https://segmentfault.com/a/1190000010093082
2017-07-07T16:39:10+08:00
2017-07-07T16:39:10+08:00
ytbean
https://segmentfault.com/u/ytbean
3
<p>原文链接:<a href="https://link.segmentfault.com/?enc=LoBed3oBfDUzGX3p2qKc6A%3D%3D.1tvF%2BBKbueQhAjg3tAfDLOioBFkaVklE0yCgUfI6epikvaH7RtatWZENGFxvhgts" rel="nofollow">《netty4.x ByteBuf 基本机制及其骨架实现》http://www.ytbean.com/posts/netty4-bytebuf/</a></p><h2>概述</h2><p>netty 是一个 NIO 框架,在 JDK API 已提供相对直接的 NIO Library 的情况下,几乎很少的软件系统会直接用 NIO 进行编程,也很少有开发者会直接使用 NIO 技术开发网络相关的程序。因为 native nio library 已饱受诟病,API 难用,容易出错,存在一些声称解决但还没解决的 bug(bug id = 6403933,JDK 1.7 声称解决了该 Bug,但实际上只是降低了该 bug 发生的概率),使用 native nio library 来开发可靠性、鲁棒性高的网络程序,工作量以及出错率都要更高,使用 netty 框架,就是为了解决这些问题。</p><h2>ByteBuffer 的忏悔</h2><p>基于 NIO 非阻塞模型的编程,基本上是面向数据容器编程,BIO 与 NIO 除了它们在阻塞 IO 线程方面有所不同外,它们在操作数据方面是有一些共性的,那就是从网络流中读数据,并放入一个<strong>容器</strong>中。<br>对于 BIO 来说,大多数时候,这个容器就是一个字节数组</p><pre><code class="java">byte[] buf = new byte[8196];
int cnt = 0;
while ((cnt = input.read(buf)) != -1) {
//......
}</code></pre><p>在这里,<strong>容器</strong>就是指 buf 这个字节数组。而在 NIO 中,容器是指 ByteBuffer,由于 NIO 编程的复杂性,需要解决类似于 TCP 半包问题等,因此对这个<strong>容器</strong>的要求不仅仅是“存储数据”那么简单,还希望这个容器能提供另外的功能,这是 ByteBuffer 存在的原因,它提供了一些方便的 API,让开发者操作底层的字节数组。<br>然而 ByteBuffer 存在几个不得人心的缺点:</p><ol><li>API 功能有限</li><li>长度固定</li><li>读写时的手工操作</li></ol><p>从 ByteBuffer 的源码里可以看到,它用 4 个下标来辅助管理自己身上的数据,参见它的父类 <code>java.nio.Buffer</code></p><p><img src="/img/remote/1460000041526679" alt="ByteBuffer 原理" title="ByteBuffer 原理"></p><p>capacity 是 ByteBuffer 的总容量,一旦设定不能改变,就像一个水缸一样,水缸的大小永远是你看到它的时候那么大,它不会变大也不会变小,最多能装多少水是确定的;<br>在这里假设一种先向 ByteBuffer 写数据后再读出来的场景。往 ByteBuffer 里写入数据时,写入多少数据,position 这个下标就会增加多少,换言之,在往 ByteBuffer 写数据时,position 指向的是下一个可以写入的位置,而 limit 此时会和 capacity 一样大。开始读数据时,第一个,要知道从哪里读,第二个,要知道读到哪里为止,为此 ByteBuffer 提供了一个 flip() 方法,这个 flip() 方法将 limit 置为 position 位置,此时 limit 代表要读到哪里为止,再将 position 位置置为 0,此时 position 代表要从哪里开始读。</p><p><img src="/img/remote/1460000041526680" alt="flip 演示" title="flip 演示"></p><p>因此,在读的时候,读取 position 到 limit 之间的数据,就能读到上一次写入的数据。但不得不说,这种方法显得有点笨拙,不太人性化,这意味着在编写代码的时候,要时刻谨记写完数据后,读数据之前,要先调用 flip 方法,这种“不著名”的潜规则,容易让开发者趟坑。</p><h2>ByteBuf 让人耳目一新</h2><p>netty 中的 ByteBuf 采用了新的做法,只用两个下标来辅助管理数据,分别是 readerIndex 和 writerIndex</p><p><img src="/img/remote/1460000041526681" alt="ByteBuf 初始下标位置" title="ByteBuf 初始下标位置"></p><p>readerIndex 代表当前读取的位置,writerIndex 代表下一个可以写入的位置,写入一部分数据后,writerIndex 往右移动,而 readerIndex 和 writeIndex 之间的数据就变为可读的了。</p><p><img src="/img/remote/1460000041526682" alt="写入一部分数据后" title="写入一部分数据后"></p><p>如果原先写入了 N 个长度的数据,接下来读取 M (M < N)个长度的数据,那么读取后 ByteBuf 就变成下面的样子</p><p><img src="/img/remote/1460000041526683" alt="读取一部分数据后" title="读取一部分数据后"></p><p>我们不再需要那笨拙的 flip 方法了,只需要关注 readerIndex 与 writerIndex。</p><h2>ByteBuf 谱系</h2><p>在 netty4.x 中,ByteBuf 是一个抽象类,但它也是在十分抽象,因为它定义的所有方法都是抽象方法,如果换我来想,我会想怎么不定义为一个 Interface 呢,ByteBuf 类也加了一个注解</p><pre><code>@SuppressWarnings("ClassMayBeInterface")</code></pre><p>但这么做其实无伤大雅,留着抽象类的身份,猜测是考虑到了以后可能增加工具类方法或者公共方法。ByteBuf 下的子类如下图所示:</p><p><img src="/img/remote/1460000041526684" alt="ByteBuf 子类" title="ByteBuf 子类"></p><p>除了 AbstractByteBuf 类,其它直接的子类都给人一种有“特殊作用”的感觉,比如说 EmptyByteBuf。最主要的类还是 AbstractByteBuf 类,它定义了大多数 ByteBuf 功能的公共逻辑代码,在 netty 应用程序的开发中,用到的 ByteBuf 的功能,以及 ByteBuf 的具体实例,都跟它有关。</p><p><img src="/img/remote/1460000041526685" alt="AbstractByteBuf 谱系" title="AbstractByteBuf 谱系"></p><h2>特殊作用的类</h2><p>与 AbstractByteBuf 同级的类有 EmptyByteBuf,UnreleasableByteBuf,SwappedByteBuf 以及 ReplayingDecoderBuffer。想要解读这些有特殊作用的类,需要先了解<strong>字节序</strong>和<strong>引用计数</strong>。</p><h3>字节序</h3><p>两个计算机系统之间通信,通过网络发送字节数据,双方必须为字节数据的顺序达成一致的协议,否则将无法对数据进行正确的解析,不同的计算机体系结构有不同的字节序,字节序可分为<strong>大端字节序</strong>(big-endian)和<strong>小端字节序</strong>(little-endian)。</p><ul><li>小端就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端</li><li>大端就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端</li></ul><p>以数字 0x12 34 56 78为例,在大端模式下,其存储的形式为:</p><pre><code>低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78</code></pre><p>小端模式下,其存储形式为:</p><pre><code>低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12</code></pre><p>一般情况下,基于 TCP 的网络通信约定采用大端字节序,而机器 CPU 的字节序则各有各的不同。</p><h3>SwappedByteBuf 与字节序</h3><p>SwappedByteBuf 这个类的命名并没有直接地反映出类的作用,在 ByteBuf 类中定义了一个方法,用于设置该 ByteBuf 中的数据采用的是哪种字节序存储数据:</p><pre><code class="java">public abstract ByteBuf order(ByteOrder endianness);</code></pre><p>netty 中的 ByteBuf 默认是使用 big-endian 的,如果需要修改字节序,意味着读写数据的时候要进行顺序的转换,一般情况下我们会直接在 ByteBuf 的读写方法里去做修改,但那样意味着要修改很多个方法,netty 的做法是为每个 ByteBuf 集成一个 SwappedByteBuf,作为自身的字节序包装器。以 AbstractByteBuf 的 order 方法为例:</p><pre><code class="java"> @Override
public ByteBuf order(ByteOrder endianness) {
if (endianness == null) {
throw new NullPointerException("endianness");
}
if (endianness == order()) {
return this;
}
SwappedByteBuf swappedBuf = this.swappedBuf;
if (swappedBuf == null) {
this.swappedBuf = swappedBuf = new SwappedByteBuf(this);
}
return swappedBuf;
}</code></pre><p>AbstractByteBuf 组合了一个 SwappedByteBuf 实例,当它的 order 方法被调用来设置字节序时,如果设置的字节序与自身的字节序不同,那么就将自己披上 SwappedByteBuf 外套,返回自身。接下来看 SwappedByteBuf 的具体实现,可以发现,SwappedByteBuf 里维护了被它包装的 ByteBuf,以及新的 ByteOrder。</p><pre><code class="java">public final class SwappedByteBuf extends ByteBuf {
private final ByteBuf buf;
private final ByteOrder order;
public SwappedByteBuf(ByteBuf buf) {
if (buf == null) {
throw new NullPointerException("buf");
}
this.buf = buf;
if (buf.order() == ByteOrder.BIG_ENDIAN) {
order = ByteOrder.LITTLE_ENDIAN;
} else {
order = ByteOrder.BIG_ENDIAN;
}
}
......
}</code></pre><p>与字节序无关的操作,都 delegate 给原来的 buf,例如:</p><pre><code class="java"> @Override
public int capacity() {
return buf.capacity();
}</code></pre><p>而与字节序有关的操作,则根据当前的字节序,对数据进行反排序处理,例如 writeInt 方法:</p><pre><code class="java"> @Override
public ByteBuf writeInt(int value) {
buf.writeInt(ByteBufUtil.swapInt(value));
return this;
}</code></pre><pre><code class="java"> /**
* Toggles the endianness of the specified 32-bit integer.
*/
public static int swapInt(int value) {
return Integer.reverseBytes(value);
}</code></pre><p>同样,除了写数据相关的方法,读数据相关的方法也是这么处理的。</p><h3>引用计数</h3><p>netty 中 ByteBuf 用来作为数据的容器,是一种频繁被创建和销毁的对象,ByteBuf 需要的内存空间,可以在 JVM Heap 中申请分配,也可以在 Direct Memory 中申请,其中在 Direct Memory 中分配的 ByteBuf,其创建和销毁的代价比在 JVM Heap 中的更高,但抛开哪个代价高哪个代价低不说,光是频繁创建和频繁销毁这一点,就已奠定了效率不高的基调。<br>netty 中支持 ByteBuf 的池化,而引用计数就是实现池化的关键技术点,不过并非只有池化的 ByteBuf 才有引用计数,非池化的也会有引用计数。<br>ByteBuf 类实现了 ReferenceCounted 接口,该接口标记一个类是一个引用计数管理对象。</p><pre><code class="java">public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf></code></pre><p>ReferenceCounted 接口定义了这几个方法:</p><pre><code class="java">public interface ReferenceCounted {
int refCnt();
ReferenceCounted retain();
ReferenceCounted retain(int increment);
boolean release();
boolean release(int decrement);
}</code></pre><p>每一个引用计数对象,都维护了自身的引用计数,当第一次被创建时,引用计数为1,通过 refCnt() 方法可以得到当前的引用计数,retain() retain(int increment) 增加自身的引用计数,而 release() 和 release(int increment) 则减少当前的引用计数,如果引用计数达到 0,并且当前的 ByteBuf 被释放成功,那这两个方法的返回值为 true。需要注意的是,各种不同类型的 ByteBuf 自己决定机子的释放方式,如果是池化的 ByteBuf,那么就会进池子,如果不是池化的,则销毁底层的字节数组引用或者释放对应的堆外内存。<br>通过 AbstractReferenceCountedByteBuf 这个类的 release 方法实现,可以看出大概的执行逻辑:</p><pre><code class="java"> @Override
public final boolean release() {
for (;;) {
int refCnt = this.refCnt;
if (refCnt == 0) {
throw new IllegalReferenceCountException(0, -1);
}
if (refCntUpdater.compareAndSet(this, refCnt, refCnt - 1)) {
if (refCnt == 1) {
deallocate();
return true;
}
return false;
}
}
}</code></pre><p>释放对象的方法定义在 deallocate() 方法里,而它是个抽象方法。<br>对于非池化的 heap ByteBuf 来说,释放对象实际上就是释放底层字节数组的引用:</p><pre><code class="java"> @Override
protected void deallocate() {
array = null;
}</code></pre><p>对于非池化的 direct ByteBuf 来说,释放对象实际上就是释放堆外内存:</p><pre><code class="java"> @Override
protected void deallocate() {
ByteBuffer buffer = this.buffer;
if (buffer == null) {
return;
}
this.buffer = null;
if (!doNotFree) {
PlatformDependent.freeDirectBuffer(buffer);
}
if (leak != null) {
leak.close();
}
}</code></pre><p>对于池化的 ByteBuf 来说,就是把自己归还到对象池里:</p><pre><code class="java"> @Override
protected final void deallocate() {
if (handle >= 0) {
final long handle = this.handle;
this.handle = -1;
memory = null;
chunk.arena.free(chunk, handle);
if (leak != null) {
leak.close();
} else {
recycle();
}
}
}</code></pre><h3>UnreleasableByteBuf 与引用计数</h3><p>顾名思义,这个类就是不可释放的 ByteBuf,它也是一个包装器模式的引用,被它包装的 ByteBuf 不会受引用计数的影响,不会被释放,它对 ReferenceCounted 接口的实现如下所示:</p><pre><code class="java"> @Override
public ByteBuf retain(int increment) {
return this;
}
@Override
public ByteBuf retain() {
return this;
}
@Override
public boolean isReadable(int size) {
return buf.isReadable(size);
}
@Override
public boolean isWritable(int size) {
return buf.isWritable(size);
}
@Override
public int refCnt() {
return buf.refCnt();
}</code></pre><p>可见它直接忽略了对 retain 和 release 方法的调用效果,这种“不可释放的 ByteBuf”在什么情况下会用到呢,在一些静态的具有固定内容并且内容不改变的 ByteBuf 时候会用到,因为非常常用,所以不需要释放,会更有效率。例如在处理 HTTP 协议时候,经常需要返回带有回车换行的数据,这里回车换行就可以定义为一个静态的 ByteBuf,并且不允许释放。这有点类似于设计模式中单例模式的那个“单例”。</p><h3>EmptyByteBuf</h3><p>EmptyByteBuf 是一个没有任何内容,也不允许读或者写的 ByteBuf,它存在的目的是为了在调用 ByteBufAllocator 创建新 ByteBuf 的时候,如果指定容量大小为0,则返回一个 EmptyByteBuf,这里仅仅是单例模式的一个运用</p><h3>ReplayingDecoderBuffer</h3><p>这个 ByteBuf 专用于 ReplayingDecoder,这个 decoder 主要是为了完成对一段已知长度报文进行全包获取,因为这个场景在网络编程中太常用了,因此 netty 单独实现了一个 ReplayingDecoder 来应对这种场景。这里暂时不深入讲解 ReplayingDecoder。</p><h2>ByteBuf 骨架实现</h2><p>AbstractByteBuf 是 ByteBuf 的骨架实现,它实现了大部分与 ByteBuf 有关的功能方法,把不确定的行为留为抽象方法,交给它的实现者去实现。</p><h3>setter 与 getter</h3><p>为了实践面向对象<strong>封装</strong>的特性,见过太多类在定义其变量的 setter 和 getter 方法时,清一色地使用 <strong>setXXX(int xxx)</strong>和 <strong>getXXX()</strong>。不过 netty 的编码风格中,它的 setter 和 getter 方法是这样的:</p><pre><code class="java">public ByteBuf readerIndex(int readerIndex); // setter
public int readerIndex(); //getter</code></pre><p>方法名同名,但参数列表和返回值不一样。并且对于 setter 类方法来说,它支持更加 modern 的做法,那就是方法的链式调用,setter 后返回自身,立马可以进行下一次方法调用。<br>但在 AbstractByteBuf 中还是有以 set 开头的的方法的,比如说:</p><pre><code class="java"> @Override
public ByteBuf setIndex(int readerIndex, int writerIndex) {
if (readerIndex < 0 || readerIndex > writerIndex || writerIndex > capacity()) {
throw new IndexOutOfBoundsException(String.format(
"readerIndex: %d, writerIndex: %d (expected: 0 <= readerIndex <= writerIndex <= capacity(%d))",
readerIndex, writerIndex, capacity()));
}
this.readerIndex = readerIndex;
this.writerIndex = writerIndex;
return this;
}</code></pre><p>而其它的 set 开头的方法,则不能说它是 setter 了,因为这些方法实际上是在操作数据,为某个下标位置填入数据,例如:</p><pre><code class="java">public ByteBuf setByte(int index, int value);</code></pre><h3>读取数据</h3><p>AbstractByteBuf 中有两类读取数据的方法,一类以 <strong>get</strong> 开头,例如 getInt(),另一类以 <strong>read</strong> 开头,例如readInt()。这两者的区别是,get 不会导致 readerIndex 的增加,而 read 会导致 readerIndex 的增加;另一个区别是,read 只能读取已经被写入的数据,也就是说,读取的位置不能超过 writeIndex,而 get 却可以在任意位置读取,只要不超过 capacity 就可以。通过以下代码可以看出这两点区别:</p><pre><code class="java"> @Override
public int getInt(int index) {
checkIndex(index, 4);
return _getInt(index);
}
protected final void checkIndex(int index, int fieldLength) {
ensureAccessible();
if (fieldLength < 0) {
throw new IllegalArgumentException("length: " + fieldLength + " (expected: >= 0)");
}
if (index < 0 || index > capacity() - fieldLength) {
throw new IndexOutOfBoundsException(String.format(
"index: %d, length: %d (expected: range(0, %d))", index, fieldLength, capacity()));
}
}
@Override
public int readInt() {
checkReadableBytes(4);
int v = _getInt(readerIndex);
readerIndex += 4;
return v;
}
protected final void checkReadableBytes(int minimumReadableBytes) {
ensureAccessible();
if (minimumReadableBytes < 0) {
throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");
}
if (readerIndex > writerIndex - minimumReadableBytes) {
throw new IndexOutOfBoundsException(String.format(
"readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
readerIndex, minimumReadableBytes, writerIndex, this));
}
}</code></pre><p>在这里也体现了出了先前提到的引用计数的作用,在读取的时候,会调用 ensureAccessible() 方法来确定当前自己的引用计数是多少。如果是 0,则此次读取时非法的。</p><pre><code class="java"> protected final void ensureAccessible() {
if (refCnt() == 0) {
throw new IllegalReferenceCountException(0);
}
}</code></pre><p>同样,并非只有读取数据才会判断引用计数,写入数据的时候也会判断引用计数。<br>真正读取数据的方法,定义成了抽象方法,供不同的实现者去实现,例如 _getInt() 方法,Heap ByteBuf 的实现是直接读取底层的数组:</p><pre><code class="java"> @Override
protected int _getInt(int index) {
return (array[index] & 0xff) << 24 |
(array[index + 1] & 0xff) << 16 |
(array[index + 2] & 0xff) << 8 |
array[index + 3] & 0xff;
}</code></pre><p>而 Direct ByteBuf,则是委托给了 ByteBuffer :</p><pre><code class="java"> @Override
protected int _getInt(int index) {
return buffer.getInt(index);
}</code></pre><h3>写入数据</h3><p>与读取数据一样,写入数据也分为改变 writerIndex 和不改变 writerIndex 的方法,分别是 write 开头和 set 开头。其中 set 开头的方法和读取数据时的 get 开头的方法一样,都只是检查一下有没有超过 capacity,并不会去检查 writerIndex 或者是 readerIndex,相当于说这些方法可以在任意一个地方写入数据,只要不超过 capacity,如下所示:</p><pre><code class="java"> @Override
public ByteBuf setInt(int index, int value) {
checkIndex(index, 4);
_setInt(index, value);
return this;
}</code></pre><p>而 write 开头的方法的调用,则会对应着 writerIndex 的增长:</p><pre><code class="java"> @Override
public ByteBuf writeInt(int value) {
ensureWritable(4);
_setInt(writerIndex, value);
writerIndex += 4;
return this;
}</code></pre><p>注意在这里,写入操作还伴随着对是否有足够的空间写入的确定,继而伴随着 ByteBuf 的动态扩容。</p><h4>ByteBuf 动态扩容机制</h4><p>如果当前没有足够的空间写入数据了,ByteBuffer 会直接报错,而 ByteBuf 则会进行动态扩容,其扩容的主要逻辑在以下的方法里:</p><pre><code class="java"> @Override
public ByteBuf ensureWritable(int minWritableBytes) {
if (minWritableBytes < 0) {
throw new IllegalArgumentException(String.format(
"minWritableBytes: %d (expected: >= 0)", minWritableBytes));
}
if (minWritableBytes <= writableBytes()) {
return this;
}
if (minWritableBytes > maxCapacity - writerIndex) {
throw new IndexOutOfBoundsException(String.format(
"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
writerIndex, minWritableBytes, maxCapacity, this));
}
// Normalize the current capacity to the power of 2.
int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes);
// Adjust to the new capacity.
capacity(newCapacity);
return this;
}</code></pre><p>首先的前提是,扩容虽好,但并不意味着可以无限扩容,因此有一个 maxCapaciy 变量限制着:你可以扩容,但不可以无限扩容,我允许你走进我的世界,但不允许你在我的世界里走来走去。<br>扩容的逻辑主要分为两块:</p><ol><li>计算新的容量</li><li>扩展至新容量</li></ol><p>计算新容量的方法如下所示:</p><pre><code class="java"> private int calculateNewCapacity(int minNewCapacity) {
final int maxCapacity = this.maxCapacity;
final int threshold = 1048576 * 4; // 4 MiB page
if (minNewCapacity == threshold) {
return threshold;
}
// If over threshold, do not double but just increase by threshold.
if (minNewCapacity > threshold) {
int newCapacity = minNewCapacity / threshold * threshold;
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
} else {
newCapacity += threshold;
}
return newCapacity;
}
// Not over threshold. Double up to 4 MiB, starting from 64.
int newCapacity = 64;
while (newCapacity < minNewCapacity) {
newCapacity <<= 1;
}
return Math.min(newCapacity, maxCapacity);
}</code></pre><p>计算新容量的逻辑很简单,如果期望的新容量不超过 4MB,则从 64 字节开始,一直翻倍,直到超过期望的新容量,此时新的容量不大于 4MB,并且是 64 的倍数。如果期望的新容量已经超过了 4MB,那么就再增加 4 MB 的倍数,至于是1倍还是2倍还是N倍,由期望的容量决定。<br>计算完新的容量,接下来就需要把 ByteBuf 的容量扩展至新的容量,扩展容量对于不同类型的 ByteBuf 来说,其实现方式也不一样,例如对于 Heap ByteBuf 来说,扩容就意味着数组拷贝,如下所示:</p><pre><code class="java"> @Override
public ByteBuf capacity(int newCapacity) {
ensureAccessible();
if (newCapacity < 0 || newCapacity > maxCapacity()) {
throw new IllegalArgumentException("newCapacity: " + newCapacity);
}
int oldCapacity = array.length;
if (newCapacity > oldCapacity) {
byte[] newArray = new byte[newCapacity];
System.arraycopy(array, readerIndex(), newArray, readerIndex(), readableBytes());
setArray(newArray);
} else if (newCapacity < oldCapacity) {
byte[] newArray = new byte[newCapacity];
int readerIndex = readerIndex();
if (readerIndex < newCapacity) {
int writerIndex = writerIndex();
if (writerIndex > newCapacity) {
writerIndex(writerIndex = newCapacity);
}
System.arraycopy(array, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
} else {
setIndex(newCapacity, newCapacity);
}
setArray(newArray);
}
return this;
}</code></pre><p>这是 ByteBuf 比 ByteBuffer 更好的一个地方,既有 maxCapacity 防止无限扩容,又能在允许的范围内动态扩展容量,开发者无须关心。至于扩展的梯段为什么是 4MB,还没办法知道这个值是怎么来的,应该是经过大量的测试或者以经验来判断的。</p><h3>丢弃一部分数据</h3><p>前面提到一张图,当写入数据后读取一部分数据,被读取后的那一部分,实际上就变成了可以丢弃的数据了,否则就会有一种“占着茅坑不拉shi”的感觉了,白白占用了大量的空间</p><p><img src="/img/remote/1460000041526683" alt="可以丢弃的数据" title="可以丢弃的数据"></p><p>AbstractByteBuf 提供了方法来对这些数据进行丢弃,原理其实就是将有效的数据移位,重置 readerIndex 和 writerIndex,对于 Heap ByteBuf 来说,这通常也意味着数组拷贝。</p><pre><code class="java"> @Override
public ByteBuf discardReadBytes() {
ensureAccessible();
if (readerIndex == 0) {
return this;
}
if (readerIndex != writerIndex) {
setBytes(0, this, readerIndex, writerIndex - readerIndex);
writerIndex -= readerIndex;
adjustMarkers(readerIndex);
readerIndex = 0;
} else {
adjustMarkers(readerIndex);
writerIndex = readerIndex = 0;
}
return this;
}</code></pre><p>通常,<strong>数组拷贝</strong>是一个关于性能的敏感词,过多的数组拷贝,意味着效率低,因此除非能确认可以丢弃的数据占整个 ByteBuf 的大部分,否则不要轻易去显式丢弃那些已经读取的数据。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Java Trouble Shooting
https://segmentfault.com/a/1190000009175108
2017-04-24T14:17:11+08:00
2017-04-24T14:17:11+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<h2>线程栈</h2><h3>什么是线程栈(thread dump)</h3><p>线程栈是某个时间点,JVM所有线程的活动状态的一个汇总;通过线程栈,可以查看某个时间点,各个线程正在做什么,通常使用线程栈来定位软件运行时的各种问题,例如 CPU 使用率特别高,或者是响应很慢,性能大幅度下滑。<br>线程栈包含了多个线程的活动信息,一个线程的活动信息通常看起来如下所示:</p><pre><code>"main" prio=10 tid=0x00007faac0008800 nid=0x9f0 waiting on condition [0x00007faac6068000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at ThreadDump.main(ThreadDump.java:4)</code></pre><p>这条线程的线程栈信息包含了以下这些信息:</p><ul><li>线程的名字:其中 <strong>main</strong> 就是线程的名字,需要注意的是,当使用 <code>Thread</code> 类来创建一条线程,并且没有指定线程的名字时,这条线程的命名规则为 <strong>Thread-i</strong>,i 代表数字。如果使用 <code>ThreadFactory</code> 来创建线程,则线程的命名规则为 <strong> pool-i-thread-j</strong>,i 和 j 分别代表数字。</li><li>线程的优先级:<strong>prio=10</strong> 代表线程的优先级为 10</li><li>线程 id:<strong>tid=0x00007faac0008800</strong> 代表线程 id 为 0x00007faac0008800,而<strong> nid=0x9f0</strong> 代表该线程对应的操作系统级别的线程 id。所谓的 nid,换种说法就是 native id。在操作系统中,分为内核级线程和用户级线程,JVM 的线程是用户态线程,内核不知情,但每一条 JVM 的线程都会映射到操作系统一条具体的线程</li><li>线程的状态:<strong>java.lang.Thread.State: TIMED_WAITING (sleeping)</strong> 以及 <strong>waiting on condition</strong> 代表线程当前的状态</li><li>线程占用的内存地址:<strong>[0x00007faac6068000]</strong> 代表当前线程占用的内存地址</li><li>线程的调用栈:<em>at java.lang.Thread.sleep(Native Method)</em>* 以及它之后的相类似的信息,代表线程的调用栈</li></ul><h3>回顾线程状态</h3><p><img src="/img/remote/1460000041526782" alt="thread-state-diagram.png" title="thread-state-diagram.png"></p><ul><li>NEW:线程初创建,未运行</li><li>RUNNABLE:线程正在运行,但<strong>不一定消耗 CPU</strong></li><li>BLOCKED:线程正在等待另外一个线程释放锁</li><li>WAITING:线程执行了 <code>wait, join, park</code> 方法</li><li>TIMED_WAITING:线程调用了<code>sleep, wait, join, park</code> 方法,与 WAITING 状态不同的是,这些方法带有表示时间的参数。</li></ul><p>例如以下代码:</p><pre><code class="java">public static void main(String[] args) throws InterruptedException {
int sum = 0;
while (true) {
int i = 0;
int j = 1;
sum = i + j;
}
}</code></pre><p>main 线程对应的线程栈就是</p><pre><code class="java">"main" prio=10 tid=0x00007fe1b4008800 nid=0x1292 runnable [0x00007fe1bd88f000]
java.lang.Thread.State: RUNNABLE
at ThreadDump.main(ThreadDump.java:7)</code></pre><p>其状态为 RUNNABLE</p><p>如果是以下代码,两个线程会竞争同一个锁,其中只有一个线程能获得锁,然后进行 sleep(time),从而进入 TIMED_WAITING 状态,另外一个线程由于等待锁,会进入 BLOCKED 状态。</p><pre><code class="java"> public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
fun1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.setDaemon(false);
t1.setName("MyThread1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
fun2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.setDaemon(false);
t2.setName("MyThread2");
t1.start();
t2.start();
*/
}
private static synchronized void fun1() throws InterruptedException {
System.out.println("t1 acquire");
Thread.sleep(Integer.MAX_VALUE);
}
private static synchronized void fun2() throws InterruptedException {
System.out.println("t2 acquire");
Thread.sleep(Integer.MAX_VALUE);
}</code></pre><p>对应的线程栈为:</p><pre><code class="java">"MyThread2" prio=10 tid=0x00007ff1e40b1000 nid=0x12eb waiting for monitor entry [0x00007ff1e07f6000]
java.lang.Thread.State: BLOCKED (on object monitor)
at ThreadDump.fun2(ThreadDump.java:45)
- waiting to lock <0x00000000eb8602f8> (a java.lang.Class for ThreadDump)
at ThreadDump.access$100(ThreadDump.java:1)
at ThreadDump$2.run(ThreadDump.java:25)
at java.lang.Thread.run(Thread.java:745)
"MyThread1" prio=10 tid=0x00007ff1e40af000 nid=0x12ea waiting on condition [0x00007ff1e08f7000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at ThreadDump.fun1(ThreadDump.java:41)
- locked <0x00000000eb8602f8> (a java.lang.Class for ThreadDump)
at ThreadDump.access$000(ThreadDump.java:1)
at ThreadDump$1.run(ThreadDump.java:10)
at java.lang.Thread.run(Thread.java:745)</code></pre><p>可以看到,t1 线程的调用栈里有这么一句 <strong> - locked <0x00000000eb8602f8> (a java.lang.Class for ThreadDump)</strong>,说明它获得了锁,并且进行 sleep(sometime) 操作,因此状态为 TIMED_WAITING。而 t2 线程由于获取不到锁,所以在它的调用栈里能看到 <strong>- waiting to lock <0x00000000eb8602f8> (a java.lang.Class for ThreadDump)</strong>,说明它正在等待锁,因此进入 BLOCKED 状态。</p><p>对于 WAITING 状态的线程栈,可以使用以下代码来模拟制造:</p><pre><code class="java"> private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
synchronized (lock) {
lock.wait();
}
}</code></pre><p>得到的线程栈为:</p><pre><code class="java">"main" prio=10 tid=0x00007f1fdc008800 nid=0x13fe in Object.wait() [0x00007f1fe1fec000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000eb860640> (a java.lang.Object)
at java.lang.Object.wait(Object.java:503)
at ThreadDump.main(ThreadDump.java:7)
- locked <0x00000000eb860640> (a java.lang.Object)</code></pre><h3>如何输出线程栈</h3><p>由于线程栈反映的是 JVM 在某个时间点的线程状态,因此分析线程栈时,为避免偶然性,有必要多输出几份进行分析。以下以 HOT SPOT JVM 为例,首先可以通过以下两种方式得到 JVM 的进程 ID。</p><ol><li><p>jps 命令</p><pre><code>[root@localhost ~]# jps
5163 ThreadDump
5173 Jps</code></pre></li><li><p>ps -ef | grep java</p><pre><code>[root@localhost ~]# ps -ef | grep java
root 5163 2479 0 01:18 pts/0 00:00:00 java ThreadDump
root 5185 2553 0 01:18 pts/1 00:00:00 grep --color=auto java</code></pre></li></ol><p>接下来通过 JDK 自带的 jstack 命令</p><pre><code>[root@localhost ~]# jstack 5163
2017-04-21 01:19:41
Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):
"Attach Listener" daemon prio=10 tid=0x00007f72b8001000 nid=0x144c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Service Thread" daemon prio=10 tid=0x00007f72d4095000 nid=0x1433 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" daemon prio=10 tid=0x00007f72d4092800 nid=0x1432 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" daemon prio=10 tid=0x00007f72d4090000 nid=0x1431 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" daemon prio=10 tid=0x00007f72d408e000 nid=0x1430 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" daemon prio=10 tid=0x00007f72d4065000 nid=0x142f in Object.wait() [0x00007f72d9b83000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000eb804858> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
- locked <0x00000000eb804858> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" daemon prio=10 tid=0x00007f72d4063000 nid=0x142e in Object.wait() [0x00007f72d9c84000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000eb804470> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:503)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
- locked <0x00000000eb804470> (a java.lang.ref.Reference$Lock)
"main" prio=10 tid=0x00007f72d4008800 nid=0x142c in Object.wait() [0x00007f72dc971000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000eb860620> (a java.lang.Object)
at java.lang.Object.wait(Object.java:503)
at ThreadDump.main(ThreadDump.java:7)
- locked <0x00000000eb860620> (a java.lang.Object)
"VM Thread" prio=10 tid=0x00007f72d405e800 nid=0x142d runnable
"VM Periodic Task Thread" prio=10 tid=0x00007f72d40a0000 nid=0x1434 waiting on condition
JNI global references: 107</code></pre><p>即可将线程栈输出到控制台。若输出信息过多,在控制台上不方便分析,则可以将输出信息重定向到文件中,如下所示:</p><pre><code>jstack 5163 > thread.stack</code></pre><p>若系统中没有 jstack 命令,因为 jstack 命令是 JDK 带的,而有的环境只安装了 JRE 环境。则可以用 kill -3 命令来代替,<code>kill -3 pid</code>。Java虚拟机提供了线程转储(Thread dump)的后门, 通过这个后门, 可以将线程堆栈打印出来。 这个后门就是通过向Java进程发送一个QUIT信号, Java虚拟机收到该信号之后, 将系<br>统当前的JAVA线程调用堆栈打印出来。</p><p>若是有运行图形界面的环境,也可以使用一些图形化的工具,例如 JVisualVM 来生成线程栈文件。</p><h3>使用线程栈定位问题</h3><h4>发现死锁</h4><p>当两个或多个线程正在等待被对方占有的锁, 死锁就会发生。 死锁会导致两个线程无法继续运行, 被永远挂起。 <br>以下代码会产生死锁</p><pre><code class="java">/**
*
*
* @author beanlam
* @version 1.0
*
*/
public class ThreadDump {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
new Thread1(lock1, lock2).start();
new Thread2(lock1, lock2).start();
}
private static class Thread1 extends Thread {
Object lock1 = null;
Object lock2 = null;
public Thread1(Object lock1, Object lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
this.setName(getClass().getSimpleName());
}
public void run() {
synchronized (lock1) {
try {
Thread.sleep(2);
} catch(Exception e) {
e.printStackTrace();
}
synchronized (lock2) {
}
}
}
}
private static class Thread2 extends Thread {
Object lock1 = null;
Object lock2 = null;
public Thread2(Object lock1, Object lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
this.setName(getClass().getSimpleName());
}
public void run() {
synchronized (lock2) {
try {
Thread.sleep(2);
} catch(Exception e) {
e.printStackTrace();
}
synchronized (lock1) {
}
}
}
}
}</code></pre><p>对应的线程栈是</p><pre><code>"Thread2" prio=10 tid=0x00007f9bf40a1000 nid=0x1472 waiting for monitor entry [0x00007f9bf8944000]
java.lang.Thread.State: BLOCKED (on object monitor)
at ThreadDump$Thread2.run(ThreadDump.java:63)
- waiting to lock <0x00000000eb860498> (a java.lang.Object)
- locked <0x00000000eb8604a8> (a java.lang.Object)
"Thread1" prio=10 tid=0x00007f9bf409f000 nid=0x1471 waiting for monitor entry [0x00007f9bf8a45000]
java.lang.Thread.State: BLOCKED (on object monitor)
at ThreadDump$Thread1.run(ThreadDump.java:38)
- waiting to lock <0x00000000eb8604a8> (a java.lang.Object)
- locked <0x00000000eb860498> (a java.lang.Object)
Found one Java-level deadlock:
=============================
"Thread2":
waiting to lock monitor 0x00007f9be4004f88 (object 0x00000000eb860498, a java.lang.Object),
which is held by "Thread1"
"Thread1":
waiting to lock monitor 0x00007f9be40062c8 (object 0x00000000eb8604a8, a java.lang.Object),
which is held by "Thread2"
Java stack information for the threads listed above:
===================================================
"Thread2":
at ThreadDump$Thread2.run(ThreadDump.java:63)
- waiting to lock <0x00000000eb860498> (a java.lang.Object)
- locked <0x00000000eb8604a8> (a java.lang.Object)
"Thread1":
at ThreadDump$Thread1.run(ThreadDump.java:38)
- waiting to lock <0x00000000eb8604a8> (a java.lang.Object)
- locked <0x00000000eb860498> (a java.lang.Object)
Found 1 deadlock.</code></pre><p>可以看到,当发生了死锁的时候,堆栈中直接打印出了死锁的信息<strong> Found one Java-level deadlock: </strong>,并给出了分析信息。</p><p>要避免死锁的问题, 唯一的办法是修改代码。死锁可能会导致整个系统的瘫痪, 具体的严重程度取决于这些线程执行的是什么性质的功能代码, 要想恢复系统, 临时也是唯一的规避办法是将系统重启。</p><h4>定位 CPU 过高的原因</h4><p>首先需要借助操作系统提供的一些工具,来定位消耗 CPU 过高的 native 线程。不同的操作系统,提供的不同的 CPU 统计命令如下所示:</p><table><thead><tr><th>操作系统</th><th>solaris</th><th>linux</th><th>aix</th></tr></thead><tbody><tr><td>命令名称</td><td>prstat -L <pid></td><td>top -p <pid></td><td>ps -emo THREAD</td></tr></tbody></table><p>以 Linux 为例,首先通过 top -p <pid> 输出该进程的信息,然后输入 H,查看所有的线程的统计情况。</p><pre><code>top - 02:04:54 up 2:43, 3 users, load average: 0.10, 0.05, 0.05
Threads: 13 total, 0 running, 13 sleeping, 0 stopped, 0 zombie
%Cpu(s): 97.74 us, 0.2 sy, 0.0 ni, 2.22 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 1003456 total, 722012 used, 281444 free, 0 buffers
KiB Swap: 2097148 total, 62872 used, 2034276 free. 68880 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3368 zmw2 25 0 256m 9620 6460 R 93.3 0.7 5:42.06 java
3369 zmw2 15 0 256m 9620 6460 S 0.0 0.7 0:00.00 java
3370 zmw2 15 0 256m 9620 6460 S 0.0 0.7 0:00.00 java
3371 zmw2 15 0 256m 9620 6460 S 0.0 0.7 0:00.00 java
3372 zmw2 15 0 256m 9620 6460 S 0.0 0.7 0:00.00 java
3373 zmw2 15 0 256m 9620 6460 S 0.0 0.7 0:00.00 java
3374 zmw2 15 0 256m 9620 6460 S 0.0 0.7 0:00.00 java
3375 zmw2 15 0 256m 9620 6460 S 0.0 0.7 0:00.00 java</code></pre><p>这个命令输出的 PID 代表的是 native 线程的 id,如上所示,id 为 3368 的 native 线程消耗 CPU 最高。在Java Thread Dump文件中, 每个线程都有tid=...nid=...的属性, 其中nid就是native thread id, 只不过nid中用16进制来表示。 例如上面的例子中3368的十六进制表示为0xd28.在Java线程中查找nid=0xd28即是本地线程对应Java线程。</p><pre><code>"main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8]
at java.lang.String.indexOf(String.java:1352)
at java.io.PrintStream.write(PrintStream.java:460)
- locked <0xc8bf87d8> (a java.io.PrintStream)
at java.io.PrintStream.print(PrintStream.java:602)
at MyTest.fun2(MyTest.java:16)
- locked <0xc8c1a098> (a java.lang.Object)
at MyTest.fun1(MyTest.java:8)
- locked <0xc8c1a090> (a java.lang.Object)
at MyTest.main(MyTest.java:26)</code></pre><p>导致 CPU 过高的原因有以下几种原因:</p><ol><li>Java 代码死循环</li><li>Java 代码使用了复杂的算法,或者频繁调用</li><li>JVM 自身的代码导致 CPU 很高</li></ol><p>如果在Java线程堆栈中找到了对应的线程ID,并且该Java线程正在执行Native code,说明导致CPU过高的问题代码在JNI调用中,此时需要打印出 Native 线程的线程栈,在 linux 下,使用 pstack <pid> 命令。<br>如果在 native 线程堆栈中可以找到对应的消耗 CPU 过高的线程 id,可以直接定位为 native 代码的问题。<br>但是有可能在 native 线程堆栈中找不到对应的消耗 CPU 过高的线程 id,这可能是因为 JNI 调用中重新创建的线程来执行, 那么在 Java 线程堆栈中就不存在该线程的信息,也有可能是虚拟机自身代码导致的 CPU 过高, 如堆内存使用过高导致的频繁 FULL GC ,或者 JVM 的 Bug。</p><h4>定位性能下降原因</h4><p>性能下降一般是由于资源不足所导致。如果资源不足, 那么有大量的线程在等待资源, 打印的线程堆栈如果发现大量的线程停在同样的调用上下文上, 那么就说明该系统资源是瓶颈。 <br>导致资源不足的原因可能有:</p><ul><li>资源数量配置太少( 如连接池连接配置过少等), 而系统当前的压力比较大, 资源不足导致了某些线程不能及时获得资源而等待在那里(即挂起)</li><li><p>获得资源的线程把持资源时间太久, 导致资源不足,例如以下代码:</p><pre><code class="java">void fun1() {
Connection conn = ConnectionPool.getConnection();//获取一个数据库连接
//使用该数据库连接访问数据库
//数据库返回结果,访问完成
//做其它耗时操作,但这些耗时操作数据库访问无关,
conn.close(); //释放连接回池
}</code></pre></li><li>设计不合理导致资源占用时间过久, 如SQL语句设计不恰当, 或者没有索引导致的数据库访问太慢等。</li><li>资源用完后, 在某种异常情况下, 没有关闭或者回池, 导致可用资源泄漏或者减少, 从而导致资源竞争。</li></ul><h4>定位系统假死原因</h4><p>导致系统挂死的原因有很多, 其中有一个最常见的原因是线程挂死。每次打印线程堆栈, 该线程必然都在同一个调用上下文上, 因此定位该类型的问题原理是,通过打印多次堆栈, 找出对应业务逻辑使用的线程, 通过对比前后打印的堆栈确认该线程执行的代码段是否一直没有执行完成。 通过打印多次堆栈, 找到挂起的线程( 即不退出)。<br>导致线程无法退出的原因可能有:</p><ul><li>线程正在执行死循环的代码</li><li>资源不足或者资源泄漏, 造成当前线程阻塞在锁对象上( 即wait在锁对象上), 长期得不到唤醒(notify)。</li><li>如果当前程序和外部通信, 当外部程序挂起无返回时, 也会导致当前线程挂起。</li></ul><h2>性能瓶颈</h2><h3>性能优化的理念</h3><p>粗略地划分,代码可分为 cpu consuming 和 io consuming 两种类型,即耗 CPU 的和耗 IO 的代码。如果当前CPU已经能够接近100%的利用率, 并且代码业务逻辑无法再简化, 那么说明该系统的已经达到了性能最大化, 如果再想提高性能, 只能增加处理器(增加更多的机器或者安装更多的CPU)。<br>而耗 IO 的代码,一般体现为<strong>请求某种资源</strong>,这可以是访问数据库,或者访问网络对端。</p><p>评价程序写得好不好,要看随着访问压力的上升,CPU 使用率的变化,好的代码,随着访问压力的上升,CPU 的使用率最终能趋近100%,而坏的代码,使用率始终无法趋近 100%,有可能在 70% 就已经上不去了。好的代码应该在代码本身效率足够高的情况下,通过使用并发等手段,让 CPU 的尽量地忙起来。随着访问压力的上升,CPU 使用率也上升,并且 CPU 所跑的代码都是已经无法再进行逻辑优化或者效率提升的代码,这是最理想的状态。</p><h3>常见性能瓶颈</h3><h4>多余的同步</h4><p>不相关的两个函数, 共用了一个锁,或者不同的共享变量共用了同一个锁, 无谓地制造出了资源争用,如下代码所示:</p><pre><code class="java">class MyClass {
Object sharedObj;
synchronized void fun1() {...} //访问共享变量sharedObj
synchronized void fun2() {...} //访问共享变量sharedObj
synchronized void fun3() {...} //不访问共享变量sharedObj
synchronized void fun4() {...} //不访问共享变量sharedObj
synchronized void fun5() {...} //不访问共享变量sharedObj
}</code></pre><p>上面的代码将sychronized加在类的每一个方法上面, 违背了保护什么锁什么的原则。对于无共享资源的两个方法, 使用了同一个锁, 人为造成了不必要的锁等待。 上述的代码可作如下修改:</p><pre><code class="java">class MyClass {
Object sharedObj;
synchronized void fun1() {...} //访问共享变量sharedObj
synchronized void fun2() {...} //访问共享变量sharedObj
void fun3() {...} //不访问共享变量sharedObj
void fun4() {...} //不访问共享变量sharedObj
void fun5() {...} //不访问共享变量sharedObj
}</code></pre><h4>锁粒度过大</h4><p>对共享资源访问完成后, 没有将后续的代码放在synchronized同步代码块之外。 这样会导致当前线程长时间无谓的占有该锁, 其它争用该锁的线程只能等待, 最终导致性能受到极大影响。如下代码所示:</p><pre><code class="java">void fun1() {
synchronized(lock){
... ... //正在访问共享资源
... ... //做其它耗时操作,但这些耗时操作与共享资源无关
}
}</code></pre><p>上面的代码, 会导致一个线程过长地占有锁, 而在这么长的时间里其它线程只能等待。应将上述代码作如下修改,在多 CPU 的环境中,可获得性能的提升:</p><pre><code class="java">void fun1() {
synchronized(lock) {
... ... //正在访问共享资源
}
... ... //其它耗时操作代码拿到synchronized代码外面
}</code></pre><h4>字符串连接的滥用</h4><pre><code class="java">String c = new String("abc") + new String("efg") + new String("12345");</code></pre><p>每一次+操作都会产生一个临时对象, 并伴随着数据拷贝, 这个对性能是一个极大的消耗。 这个写法常常成为系统的瓶颈, 如果这个地方恰好是一个性能瓶颈, 修改成StringBuffer之后, 性能会有大幅的提升。</p><h4>不恰当的线程模型</h4><p>在多线程场合下, 如果线程模型不恰当, 也会使性能低下,在网络IO的场合, 使用消息发送队列和消息接收队列来进行异步IO,性能会有显著的提升。</p><p><img src="/img/remote/1460000041526783" alt="消息接收" title="消息接收"><br><img src="/img/remote/1460000041526784" alt="消息发送" title="消息发送"></p><h4>不恰当的 GC 参数</h4><p>不恰当的GC参数设置会导致严重的性能问题,比如堆内存设置过小导致大量的 CPU 时间片被用来做垃圾回收</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
logback 配置与原理
https://segmentfault.com/a/1190000008315137
2017-02-10T18:05:25+08:00
2017-02-10T18:05:25+08:00
ytbean
https://segmentfault.com/u/ytbean
40
<p>原文链接:<a href="https://link.segmentfault.com/?enc=B7dXvOYTOCAMSSBefWL56g%3D%3D.81SdWpmjc7leqiC4%2Bk5o3ggCdV13cd7ThVQF5j2E48vxhOGSu03V2AMQddfF2bMu" rel="nofollow">《logback 配置与原理》http://www.ytbean.com/posts/logback-intro/</a></p><h2>概览</h2><p>简单地说,Logback 是一个 Java 领域的日志框架。它被认为是 Log4J 的继承人。<br>Logback 主要由三个模块组成:</p><ul><li>logback-core</li><li>logback-classic</li><li>logback-access</li></ul><p>logback-core 是其它模块的基础设施,其它模块基于它构建,显然,logback-core 提供了一些关键的通用机制。logback-classic 的地位和作用等同于 Log4J,它也被认为是 Log4J 的一个改进版,并且它实现了简单日志门面 SLF4J;而 logback-access 主要作为一个与 Servlet 容器交互的模块,比如说 tomcat 或者 jetty,提供一些与 HTTP 访问相关的功能。</p><p>目前 Logback 的使用很广泛,很多知名的开源软件都使用了 Logback作为日志框架,比如说 Akka,Apache Camel 等。</p><h2>Logback 与 Log4J</h2><p>实际上,这两个日志框架都出自同一个开发者之手,Logback 相对于 Log4J 有更多的优点</p><ul><li>同样的代码路径,Logback 执行更快</li><li>更充分的测试</li><li>原生实现了 SLF4J API(Log4J 还需要有一个中间转换层)</li><li>内容更丰富的文档</li><li>支持 XML 或者 Groovy 方式配置</li><li>配置文件自动热加载</li><li>从 IO 错误中优雅恢复</li><li>自动删除日志归档</li><li>自动压缩日志成为归档文件</li><li>支持 Prudent 模式,使多个 JVM 进程能记录同一个日志文件</li><li>支持配置文件中加入条件判断来适应不同的环境</li><li>更强大的过滤器</li><li>支持 SiftingAppender(可筛选 Appender)</li><li>异常栈信息带有包信息</li></ul><h2>快速上手</h2><p>想在 Java 程序中使用 Logback,需要依赖三个 jar 包,分别是 slf4j-api,logback-core,logback-classic。其中 slf4j-api 并不是 Logback 的一部分,是另外一个项目,但是强烈建议将 slf4j 与 Logback 结合使用。要引用这些 jar 包,在 maven 项目中引入以下3个 dependencies</p><pre><code class="xml"> <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.0.11</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.11</version>
</dependency></code></pre><h3>第一个简单的例子</h3><pre><code class="java">package io.beansoft.logback.demo.universal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
*
* @author beanlam
* @date 2017年2月9日 下午11:17:53
* @version 1.0
*
*/
public class SimpleDemo {
private static final Logger logger = LoggerFactory.getLogger(SimpleDemo.class);
public static void main(String[] args) {
logger.info("Hello, this is a line of log message logged by Logback");
}
}</code></pre><p>以上代码的运行结果是:</p><pre><code>23:19:41.131 [main] INFO i.b.l.demo.universal.SimpleDemo - Hello, this is a line of log message logged by Logback</code></pre><p>注意到这里,代码里并没有引用任何一个跟 Logback 相关的类,而是引用了 SLF4J 相关的类,这边是使用 SLF4J 的好处,在需要将日志框架切换为其它日志框架时,无需改动已有的代码。</p><p><code>LoggerFactory</code> 的 <code>getLogger()</code> 方法接收一个参数,以这个参数决定 logger 的名字,这里传入了 <code>SimpleDemo</code> 这个类的 Class 实例,那么 logger 的名字便是 <code>SimpleDemo</code> 这个类的全限定类名:<code>io.beansoft.logback.demo.universal.SimpleDemo</code></p><h3>让 Logback 打印出一些它自身的内部消息</h3><pre><code>package io.beansoft.logback.demo.universal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;
/**
*
*
* @author beanlam
* @date 2017年2月9日 下午11:31:55
* @version 1.0
*
*/
public class LogInternalStateDemo {
private static final Logger logger = LoggerFactory.getLogger(LogInternalStateDemo.class);
public static void main(String[] args) {
logger.info("Hello world");
//打印 Logback 内部状态
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
StatusPrinter.print(lc);
}
}</code></pre><p>除了打印正常的日志信息,还打印出了 Logback 自身的内部状态信息</p><pre><code>23:33:19.340 [main] INFO i.b.l.d.u.LogInternalStateDemo - Hello world
23:33:19,265 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
23:33:19,265 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
23:33:19,265 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.xml]
23:33:19,266 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Setting up default configuration.</code></pre><h2>Logger,Appenders 与 Layouts</h2><p>在 logback 里,最重要的三个类分别是</p><ul><li>Logger</li><li>Appender</li><li>Layout</li></ul><p>Logger 类位于 logback-classic 模块中, 而 Appender 和 Layout 位于 logback-core 中,这意味着, Appender 和 Layout 并不关心 Logger 的存在,不依赖于 Logger,同时也能看出, Logger 会依赖于 Appender 和 Layout 的协助,日志信息才能被正常打印出来。</p><h3>分层命名规则</h3><p>为了可以控制哪些信息需要输出,哪些信息不需要输出,logback 中引进了一个 分层 概念。每个 logger 都有一个 name,这个 name 的格式与 Java 语言中的包名格式相同。这也是前面的例子中直接把一个 class 对象传进 LoggerFactory.getLogger() 方法作为参数的原因。</p><p>logger 的 name 格式决定了多个 logger 能够组成一个树状的结构,为了维护这个分层的树状结构,每个 logger 都被绑定到一个 logger 上下文中,这个上下文负责厘清各个 logger 之间的关系。</p><p>例如, 命名为 <code>io.beansoft</code> 的 logger,是命名为 <code>io.beansoft.logback</code> 的 logger 的父亲,是命名为 <code>io.beansoft.logback.demo</code> 的 logger 的祖先。</p><p>在 logger 上下文中,有一个 root logger,作为所有 logger 的祖先,这是 logback 内部维护的一个 logger,并非开发者自定义的 logger。</p><p>可通过以下方式获得这个 logger :</p><pre><code>Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);</code></pre><p>同样,通过 logger 的 name,就能获得对应的其它 logger 实例。</p><p><code>Logger</code> 这个接口主要定义的方法有:</p><pre><code>package org.slf4j;
public interface Logger {
// Printing methods:
public void trace(String message);
public void debug(String message);
public void info(String message);
public void warn(String message);
public void error(String message);
}</code></pre><h3>日志打印级别</h3><p>logger 有日志打印级别,可以为一个 logger 指定它的日志打印级别。<br>如果不为一个 logger 指定打印级别,那么它将继承离他最近的一个有指定打印级别的祖先的打印级别。这里有一个容易混淆想不清楚的地方,如果 logger 先找它的父亲,而它的父亲没有指定打印级别,那么它会立即忽略它的父亲,往上继续寻找它爷爷,直到它找到 root logger。因此,也能看出来,要使用 logback, 必须为 root logger 指定日志打印级别。</p><p>日志打印级别从低级到高级排序的顺序是:<br><code>TRACE < DEBUG < INFO < WARN < ERROR </code><br>如果一个 logger 允许打印一条具有某个日志级别的信息,那么它也必须允许打印具有比这个日志级别更高级别的信息,而不允许打印具有比这个日志级别更低级别的信息。</p><p>举个例子:</p><pre><code>package io.beansoft.logback.demo.universal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
/**
*
*
* @author beanlam
* @date 2017年2月10日 上午12:20:33
* @version 1.0
*
*/
public class LogLevelDemo {
public static void main(String[] args) {
//这里强制类型转换时为了能设置 logger 的 Level
ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foo");
logger.setLevel(Level.INFO);
Logger barlogger = LoggerFactory.getLogger("com.foo.Bar");
// 这个语句能打印,因为 WARN > INFO
logger.warn("can be printed because WARN > INFO");
// 这个语句不能打印,因为 DEBUG < INFO.
logger.debug("can not be printed because DEBUG < INFO");
// barlogger 是 logger 的一个子 logger
// 它继承了 logger 的级别 INFO
// 以下语句能打印,因为 INFO >= INFO
barlogger.info("can be printed because INFO >= INFO");
// 以下语句不能打印,因为 DEBUG < INFO
barlogger.debug("can not be printed because DEBUG < INFO");
}
}</code></pre><p>打印结果是:</p><pre><code>00:27:19.251 [main] WARN com.foo - can be printed because WARN > INFO
00:27:19.255 [main] INFO com.foo.Bar - can be printed because INFO >= INFO</code></pre><h3>获取 logger</h3><p>在 logback 中,每个 logger 都是一个单例,调用 <code>LoggerFactory.getLogger</code> 方法时,如果传入的 logger name 相同,获取到的 logger 都是同一个实例。</p><p>在为 logger 命名时,用类的全限定类名作为 logger name 是最好的策略,这样能够追踪到每一条日志消息的来源。</p><h3>Appender 和 Layout</h3><p>在 logback 的世界中,日志信息不仅仅可以打印至 console,也可以打印至文件,甚至输出到网络流中,日志打印的目的地由 Appender 来决定,不同的 Appender 能将日志信息打印到不同的目的地去。</p><p>Appender 是绑定在 logger 上的,同时,一个 logger 可以绑定多个 Appender,意味着一条信息可以同时打印到不同的目的地去。例如,常见的做法是,日志信息既输出到控制台,同时也记录到日志文件中,这就需要为 logger 绑定两个不同的 logger。</p><p>Appender 是绑定在 logger 上的,而 logger 又有继承关系,因此一个 logger 打印信息时的目的地 Appender 需要参考它的父亲和祖先。在 logback 中,默认情况下,如果一个 logger 打印一条信息,那么这条信息首先会打印至它自己的 Appender,然后打印至它的父亲和父亲以上的祖先的 Appender,但如果它的父亲设置了 <code>additivity = false</code>,那么这个 logger 除了打印至它自己的 Appender 外,只会打印至其父亲的 Appender,因为它的父亲的 <code>additivity</code> 属性置为了 false,开始变得忘祖忘宗了,所以这个 logger 只认它父亲的 Appender;此外,对于这个 logger 的父亲来说,如果父亲的 logger 打印一条信息,那么它只会打印至自己的 Appender中(如果有的话),因为父亲已经忘记了爷爷及爷爷以上的那些父辈了。</p><p>打印的日志除了有打印的目的地外,还有日志信息的展示格式。在 logback 中,用 Layout 来代表日志打印格式。比如说,PatternLayout 能够识别以下这条格式:<br><code>%-4relative [%thread] %-5level %logger{32} - %msg%n</code><br>然后打印出来的格式效果是:<br><code>176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.</code></p><p>上面这个格式的第一个字段代表从程序启动开始后经过的毫秒数,第二个字段代表打印出这条日志的线程名字,第三个字段代表日志信息的日志打印级别,第四个字段代表 logger name,第五个字段是日志信息,第六个字段仅仅是代表一个换行符。</p><h3>参数化打印日志</h3><p>经常能看到打印日志的时候,使用以下这种方式打印日志:</p><pre><code>logger.debug("the message is " + msg + " from " + somebody);</code></pre><p>这种打印日志的方式有个缺点,就是无论日志级别是什么,程序总要先执行 <code>"the message is " + msg + " from " + somebody</code> 这段字符串的拼接操作。当 logger 设置的日志级别为比 DEBUG 级别更高级别时,DEBUG 级别的信息不回被打印出来的,显然,字符串拼接的操作是不必要的,当要拼接的字符串很大时,这无疑会带来很大的性能白白损耗。</p><p>于是,一种改进的打印日志方式被人们发现了:</p><pre><code>if(logger.isDebugEnabled()) {
logger.debug("the message is " + msg + " from " + somebody);
}</code></pre><p>这样的方式确实能避免字符串拼接的不必要损耗,但这也不是最好的方法,当日志级别为 DEBUG 时,那么打印这行消息,需要判断两次日志级别。一次是<code>logger.isDebugEnabled()</code>,另一次是 <code>logger.debug()</code> 方法内部也会做的判断。这样也会带来一点点效率问题,如果能找到更好的方法,谁愿意无视白白消耗的效率。</p><p>有一种更好的方法,那就是提供占位符的方式,以参数化的方式打印日志,例如上述的语句,可以是这样的写法:</p><pre><code>logger.debug("the message {} is from {}", msg, somebody);</code></pre><p>这样的方式,避免了字符串拼接,也避免了多一次日志级别的判断。</p><h3>logback 内部运行流程</h3><p><img src="/img/remote/1460000041526874" alt="logback 内部运行流程图" title="logback 内部运行流程图"></p><p>当应用程序发起一个记录日志的请求,例如 info() 时,logback 的内部运行流程如下所示</p><ol><li>获得过滤器链条</li><li>检查日志级别以决定是否继续打印</li><li>创建一个 <code>LoggingEvent</code> 对象</li><li>调用 Appenders</li><li>进行日志信息格式化</li><li>发送 <code>LoggingEvent</code> 到对应的目的地</li></ol><h3>有关性能问题</h3><p>关于日志系统,人们讨论得最多的是性能问题,即使是小型的应用程序,也有可能输出大量的日志。打印日志中的不当处理,会引发各种性能问题,例如太多的日志记录请求可能使磁盘 IO 成为性能瓶颈,从而影响到应用程序的正常运行。在合适的时候记录日志、以更好的方式发起日志请求、以及合理设置日志级别方面,都有可能造成性能问题。<br>关于性能问题,以下几个方面需要了解</p><ul><li>建议使用占位符的方式参数化记录日志</li><li>logback 内部机制保证 logger 在记录日志时,不必每一次都去遍历它的父辈以获得关于日志级别、Appender 的信息</li><li>在 logback 中,将日志信息格式化,以及输出到目的地,是最损耗性能的操作</li></ul><h2>logback 配置</h2><h3>配置须知</h3><h4>配置方式</h4><p>logback 提供的配置方式有以下几种:</p><ul><li>编程式配置</li><li>xml 格式</li><li>groovy 格式</li></ul><p>logback 在启动时,根据以下步骤寻找配置文件:</p><ol><li>在 classpath 中寻找 logback-test.xml文件</li><li>如果找不到 logback-test.xml,则在 classpath 中寻找 logback.groovy 文件</li><li>如果找不到 logback.groovy,则在 classpath 中寻找 logback.xml文件</li><li>如果上述的文件都找不到,则 logback 会使用 JDK 的 SPI 机制查找 META-INF/services/ch.qos.logback.classic.spi.Configurator 中的 logback 配置实现类,这个实现类必须实现 <code>Configuration</code> 接口,使用它的实现来进行配置</li><li>如果上述操作都不成功,logback 就会使用它自带的 <code>BasicConfigurator</code> 来配置,并将日志输出到 console</li></ol><p>logback-test.xml 一般用来在测试代码中打日志,如果是 maven 项目,一般把 logback-test.xml 放在 src/test/resources 目录下。maven 打包的时候也不会把这个文件打进 jar 包里。<br>logback 启动的时候解析配置文件大概需要 100 毫秒的时间,如果希望更快启动,可以采用 SPI 的方式。</p><h4>默认的配置</h4><p>前面有提到默认的配置,由 <code>BasicConfiguator</code> 类配置而成,这个类的配置可以用如下的配置文件来表示:</p><pre><code class="xml"><configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration></code></pre><h4>启动时打印状态信息</h4><p>如果 logback 在启动时,解析配置文件时,出现了需要警告的信息或者错误信息,那 logback 会自动先打印出自身的状态信息。</p><p>如果希望正常情况下也打印出状态信息,则可以使用之前提到的方式,在代码里显式地调用使其输出:</p><pre><code class="java">public static void main(String[] args) {
// assume SLF4J is bound to logback in the current environment
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
// print logback's internal status
StatusPrinter.print(lc);
...
}</code></pre><p>也可以在配置文件中,指定 configuration 的 debug 属性为 true</p><pre><code class="xml"><configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder
by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration></code></pre><p>还可以指定一个 Listener:</p><pre><code class="xml"><configuration>
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
... the rest of the configuration file
</configuration></code></pre><h4>重置默认的配置文件位置</h4><p>设置 <strong>logback.configurationFile</strong> 系统变量,可以通过 -D 参数设置,所指定的文件名必须以 .xml 或者 .groovy 作为文件后缀,否则 logback 会忽略这些文件。</p><h4>配置文件自动热加载</h4><p>要使配置文件自动重载,需要把 scan 属性设置为 true,默认情况下每分钟才会扫描一次,可以指定扫描间隔:</p><pre><code class="java"><configuration scan="true" scanPeriod="30 seconds" >
...
</configuration> </code></pre><p>注意扫描间隔要加上单位,可用的单位是 milliseconds,seconds,minutes 和 hours。如果只指定了数字,但没有指定单位,这默认单位为 milliseconds。</p><p>在 logback 内部,当设置 scan 属性为 true 后,一个叫做 <code>ReconfigureOnChangeFilter</code> 的过滤器就会被牵扯进来,它负责判断是否到了该扫描的时候,以及是否该重新加载配置。Logger 的任何一个打印日志的方法被调用时,都会触发这个过滤器,所以关于这个过滤器的自身的性能问题,变得十分重要。logback 目前采用这样一种机制,当 logger 的调用次数到达一定次数后,才真正让过滤器去做它要做的事情,这个次数默认是 16,而 logback 会在运行时根据调用的频繁度来动态调整这个数目。</p><h4>输出异常栈时也打印出 jar 包的信息</h4><p>这个属性默认是关闭,可通过以下方式开启:</p><pre><code class="xml"><configuration packagingData="true">
...
</configuration></code></pre><p>也可以通过 LoggerContext 的 setPackagingDataEnabled(boolean) 方法来开启</p><pre><code> LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
lc.setPackagingDataEnabled(true);</code></pre><h4>直接调用 JoranConfigurator</h4><p><strong>Joran</strong> 是 logback 使用的一个配置加载库,如果想要重新实现 logback 的配置机制,可以直接调用这个类 <code>JoranConfigurator</code> 来实现:</p><pre><code class="java">package chapters.configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
public class MyApp3 {
final static Logger logger = LoggerFactory.getLogger(MyApp3.class);
public static void main(String[] args) {
// assume SLF4J is bound to logback in the current environment
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(args[0]);
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
logger.info("Entering application.");
Foo foo = new Foo();
foo.doIt();
logger.info("Exiting application.");
}
}</code></pre><h3>配置文件格式</h3><h4>配置文件的基本结构</h4><p><img src="/img/remote/1460000041526875" alt="配置文件基本结构" title="配置文件基本结构"></p><p>根节点是 configuration,可包含0个或多个 appender,0个或多个 logger,最多一个 root。</p><h4>配置 logger 节点</h4><p>在配置文件中,logger 的配置在<logger> 标签中配置,<logger> 标签只有一个属性是一定要的,那就是 name,除了 name 属性,还有 level 属性,additivity 属性可以配置,不过它们是可选的。<br>level 的取值可以是 <code>TRACE, DEBUG, INFO, WARN, ERROR, ALL, OFF, INHERITED, NULL</code>, 其中 <code>INHERITED</code> 和 <code>NULL</code> 的作用是一样的,并不是不打印任何日志,而是强制这个 logger 必须从其父辈继承一个日志级别。<br>additivity 的取值是一个布尔值,true 或者 false。</p><p><logger> 标签下只有一种元素,那就是 <appender-ref>,可以有0个或多个,意味着绑定到这个 logger 上的 Appender。</p><h4>配置 root 节点</h4><p><root> 标签和 <logger> 标签的配置类似,只不过 <root> 标签只允许一个属性,那就是 level 属性,并且它的取值范围只能取 <code>TRACE, DEBUG, INFO, WARN, ERROR, ALL, OFF</code>。<br><root> 标签下允许有0个或者多个 <appender-ref>。</p><h4>配置 appender 节点</h4><p><appender> 标签有两个必须填的属性,分别是 name 和 class,class 用来指定具体的实现类。<appender> 标签下可以包含至多一个 <layout>,0个或多个 <encoder>,0个或多个 <filter>,除了这些标签外,<appender> 下可以包含一些类似于 JavaBean 的配置标签。</p><p><layout> 包含了一个必须填写的属性 class,用来指定具体的实现类,不过,如果该实现类的类型是 <code>PatternLayout</code> 时,那么可以不用填写。<layout> 也和 <appender> 一样,可以包含类似于 JavaBean 的配置标签。<br><encoder> 标签包含一个必须填写的属性 class,用来指定具体的实现类,如果该类的类型是 <code>PatternLayoutEncoder</code> ,那么 class 属性可以不填。<br>如果想要往一个 logger 上绑定 appender,则使用以下方式:</p><pre><code class="xml"><logger name="HELLO" level="debug">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
</logger>
</code></pre><h4>设置 Context Name</h4><pre><code class="xml"><configuration>
<contextName>myAppName</contextName>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %contextName [%t] %level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration></code></pre><h4>变量替换</h4><p>在 logback 中,支持以 ${varName} 来引用变量</p><h5>定义变量</h5><p>可以直接在 logback.xml 中定义变量</p><pre><code><configuration>
<property name="USER_HOME" value="/home/sebastien" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${USER_HOME}/myApp.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><p>也可以通过大D参数来定义</p><pre><code>java -DUSER_HOME="/home/sebastien" MyApp2</code></pre><p>也可以通过外部文件来定义</p><pre><code><configuration>
<property file="src/main/java/chapters/configuration/variables1.properties" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${USER_HOME}/myApp.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><p>外部文件也支持 classpath 中的文件</p><pre><code><configuration>
<property resource="resource1.properties" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${USER_HOME}/myApp.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><p>外部文件的格式是 key-value 型。</p><pre><code>USER_HOME=/home/sebastien</code></pre><h5>变量的作用域</h5><p>变量有三个作用域</p><ul><li>local</li><li>context</li><li>system</li></ul><p>local 作用域在配置文件内有效,context 作用域的有效范围延伸至 logger context,system 作用域的范围最广,整个 JVM 内都有效。</p><p>logback 在替换变量时,首先搜索 local 变量,然后搜索 context,然后搜索 system。</p><p>如何为变量指定 scope ?</p><pre><code><configuration>
<property scope="context" name="nodeId" value="firstNode" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>/opt/${nodeId}/myApp.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><h5>变量的默认值</h5><p>在引用一个变量时,如果该变量未定义,那么可以为其指定默认值,做法是:</p><pre><code>${aName:-golden}</code></pre><h5>运行时定义变量</h5><p>需要使用 <define> 标签,指定接口 <code>PropertyDfiner</code> 对应的实现类。如下所示:</p><pre><code><configuration>
<define name="rootLevel" class="a.class.implementing.PropertyDefiner">
<shape>round</shape>
<color>brown</color>
<size>24</size>
</define>
<root level="${rootLevel}"/>
</configuration></code></pre><h5>条件化处理配置文件</h5><p>logback 允许在配置文件中定义条件语句,以决定配置的不同行为,具体语法格式如下:</p><pre><code> <!-- if-then form -->
<if condition="some conditional expression">
<then>
...
</then>
</if>
<!-- if-then-else form -->
<if condition="some conditional expression">
<then>
...
</then>
<else>
...
</else>
</if></code></pre><p>示例:</p><pre><code><configuration debug="true">
<if condition='property("HOSTNAME").contains("torino")'>
<then>
<appender name="CON" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>
<root>
<appender-ref ref="CON" />
</root>
</then>
</if>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${randomOutputDir}/conditional.log</file>
<encoder>
<pattern>%d %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>
<root level="ERROR">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><h5>从JNDI 获取变量</h5><p>使用 <insertFromJNDI> 可以从 JNDI 加载变量,如下所示:</p><pre><code><configuration>
<insertFromJNDI env-entry-name="java:comp/env/appName" as="appName" />
<contextName>${appName}</contextName>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d ${CONTEXT_NAME} %level %msg %logger{50}%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</configuration></code></pre><h5>文件包含</h5><p>可以使用 ≶include> 标签在一个配置文件中包含另外一个配置文件,如下图所示:</p><pre><code><configuration>
<include file="src/main/java/chapters/configuration/includedConfig.xml"/>
<root level="DEBUG">
<appender-ref ref="includedConsole" />
</root>
</configuration></code></pre><p>被包含的文件必须有以下格式:</p><pre><code><included>
<appender name="includedConsole" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>"%d - %m%n"</pattern>
</encoder>
</appender>
</included></code></pre><p>支持从多种源头包含<br>从文件中包含</p><pre><code><include file="src/main/java/chapters/configuration/includedConfig.xml"/></code></pre><p>从 classpath 中包含</p><pre><code><include resource="includedConfig.xml"/></code></pre><p>从 URL 中包含</p><pre><code><include url="http://some.host.com/includedConfig.xml"/></code></pre><p>如果包含不成功,那么 logback 会打印出一条警告信息,如果不希望 logback 抱怨,只需这样做:</p><pre><code><include optional="true" ..../></code></pre><h4>添加一个 Context Listener</h4><p><code>LoggerContextListener</code> 接口的实例能监听 logger context 上发生的事件,比如说日志级别的变化,添加的方式如下所示:</p><pre><code><configuration debug="true">
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
....
</configuration></code></pre><h2>Appender</h2><h3>Appender 概念</h3><p>当一个记录日志的事件被发起时,logback 会将这个事件发送给 appender。appender 必须实现 <code>ch.qos.logback.core.Appender</code> 接口。这个接口的最重要的方法如下所示:</p><pre><code>package ch.qos.logback.core;
import ch.qos.logback.core.spi.ContextAware;
import ch.qos.logback.core.spi.FilterAttachable;
import ch.qos.logback.core.spi.LifeCycle;
public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable {
public String getName();
public void setName(String name);
void doAppend(E event);
}</code></pre><p>这个接口的大多数方法都是 setter 和 getter,doAppend 方式是最重要的一个。这个类接收了一个泛型类型 E,在 logback-classic 模块里, E 的类型是 ILoggingEvent,在 logback-access 模块里,E 的类型是 AccessEvent。这个接口也继承了 FilterAttachable 接口,意味着可以为这个 appender 关联一个或者多个过滤器。appender 最终会根据 layout 或者 encoder 将日志信息格式化,并且只能关联一个 layout 或者 encoder,不过有一些 appender 已经有了内嵌的 layout 或者 encoder,比如说 SocketAppender。</p><h3>AppenderBase 源码分析</h3><p><code> ch.qos.logback.core.AppenderBase</code> 这个类是一个抽象类,是其它具体 appender 的父类。</p><pre><code>public synchronized void doAppend(E eventObject) {
// prevent re-entry.
if (guard) {
return;
}
try {
guard = true;
if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus(
"Attempted to append to non started appender [" + name + "].",this));
}
return;
}
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}
// ok, we now invoke the derived class's implementation of append
this.append(eventObject);
} finally {
guard = false;
}
}</code></pre><p>首先 doAppend 方法是 synchronized,保证了多线程访问这个 appender 时能起到互斥的作用,logback 也有非互斥的实现,<code> ch.qos.logback.core.UnsynchronizedAppenderBase</code>。<br>guard 变量的作用是防止同一个线程内递归调用 doAppend 方法,造成死循环从而导致栈溢出。</p><h3>logback-core 模块</h3><p>logback-core 模块是其它模块的基础设施,以下将讨论几个 logback 自带的 appender。</p><h4>OutputStreamAppender</h4><p><code>OutputStreamAppender</code> 将日志记录到一个 <code>java.io.OutputStream</code> 里,这个 appender 是其它 appender 的基础,不能直接在 logback 配置文件里面把它进行配置然后拉出来用。但这并不意味着它没有可配置的属性。</p><table><thead><tr><th>属性名</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>encoder</td><td>Encoder</td><td>日志格式</td></tr><tr><td>immediateFlush</td><td>boolean</td><td>默认值是 true,意味着日志信息直接刷到目的地,不会做缓存。如果该值设置为 false,并且没有关闭 appender,那么一部分日志信息将会丢失</td></tr></tbody></table><p><code>OutputStreamAppender</code> 的类继承体系如下图所示:<br><img src="/img/remote/1460000041526876" alt="OutputStreamAppender 继承体系" title="OutputStreamAppender 继承体系"></p><h4>ConsoleAppender</h4><p><code>ConsoleAppender</code> 将日志信息输出到控制台,既可以是 System.out,也可以是 System.err。默认是前者。</p><table><thead><tr><th>属性名</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>encoder</td><td>Encoder</td><td>日志格式</td></tr><tr><td>target</td><td>String</td><td>要么填System.out,要么填System.err,默认是前者。</td></tr><tr><td>withJansi</td><td>boolean</td><td>默认值是 false,主要作用是为代码着色,如果是 windows 平台,设置为 true 的同时还要在 classpath 中加入 "org.fusesource.jansi:jansi:1.9",Unix 类系统本身支持因此不用。如果想在 Eclipse 里面用,还要使用这个插件:ANSI in Eclipse Console</td></tr></tbody></table><p>配置示例:</p><pre><code class="xml"><configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration></code></pre><h4>FileAppender</h4><p>把日志信息记录到文件里。</p><table><thead><tr><th>属性名</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>append</td><td>boolean</td><td>如果目标文件已存在,要么追加到文件尾部,要么直接清空文件,默认是 true</td></tr><tr><td>encoder</td><td>Encoder</td><td>日志格式</td></tr><tr><td>file</td><td>String</td><td>文件名,如果文件不存在,则重新创建,如果父目录不存在,也会重新创建,注意在 windows 下,这种写法 c:\temp\test.log 会有问题,因为 \t 会被转义,正确的写法是:c:/temp/test.log 或者 c:\\temp\\test.log</td></tr><tr><td>prudent</td><td>boolean</td><td>允许多个进程向同一个日志文件里写日志,但由于锁竞争,性能消耗大概是普通日志记录的三倍左右</td></tr></tbody></table><p>默认情况下,<strong>Immediate Flush</strong> 为true,为了不丢失日志。但是也可以将其设置为 false。<br>配置示例:</p><pre><code class="xml"><configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>testFile.log</file>
<append>true</append>
<!-- set immediateFlush to false for much higher logging throughput -->
<immediateFlush>true</immediateFlush>
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><p>如果希望为日志加一个时间戳,则可以通过下面的方式来配置:</p><pre><code class="xml"><configuration>
<!-- Insert the current time formatted as "yyyyMMdd'T'HHmmss" under
the key "bySecond" into the logger context. This value will be
available to all subsequent configuration elements. -->
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<!-- use the previously created timestamp to create a uniquely
named log file -->
<file>log-${bySecond}.txt</file>
<encoder>
<pattern>%logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><p><timestamp>标签有两个必要的属性,分别是 name 和 datePattern,还有一个可选的属性 timeReference,name 所指定的可以用来作为变量使用,dataPattern 的格式与 SimpleDateFormat 的格式一样,而 timeReference 默认是配置文件被解析的时间,也可以显式指定为 contextBirth,这样时间就是上下文被创建的时间。<br>如下例子:</p><pre><code class="xml"><configuration>
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"
timeReference="contextBirth"/>
...
</configuration></code></pre><h4>RollingFileAppender</h4><p><code>RollingFileAppender</code> 既可以记录日志到文件,也可以在某个条件到达后,转移到另外一个文件。有两个子组件,分别是 RollingPolicy 和 TriggeringPolicy,分别描述了如何滚动,以及何时滚动。<br>属性:</p><table><thead><tr><th>属性名</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>file</td><td>String</td><td>见 FileAppender</td></tr><tr><td>append</td><td>boolean</td><td>见 FileAppender</td></tr><tr><td>encoder</td><td>Encoder</td><td>日志格式</td></tr><tr><td>rollingPolicy</td><td>RollingPolicy</td><td>如何滚动</td></tr><tr><td>triggeringPolicy</td><td>TriggeringPolicy</td><td>何时滚动</td></tr><tr><td>prudent</td><td>boolean</td><td>FixedWindowRollingPolicy 不支持 prudent 模式。TimeBasedRollingPolicy 支持 prudent 模式,不过有两个限制。在 prudent 模式下,无法进行文件压缩,file 属性必须留白,不能填写。</td></tr></tbody></table><h5>滚动策略</h5><p>RollingPolicy 主要负责文件的移动和重命名</p><pre><code>package ch.qos.logback.core.rolling;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.spi.LifeCycle;
public interface RollingPolicy extends LifeCycle {
public void rollover() throws RolloverFailure;
public String getActiveFileName();
public CompressionMode getCompressionMode();
public void setParent(FileAppender appender);
}</code></pre><h5>TimeBasedRollingPolicy</h5><p>这个策略基于时间,比如说基于 day 或者 month 的滚动,<strong>同时实现了 RollingPolicy 接口和 TriggeringPolicy 接口</strong>,既定义了如何滚动,也定义了何时滚动。<br><code>TimeBasedRollingPolicy</code> 只有一个必须的属性 fileNamePattern 和几个可选的属性。</p><p>fileNamePattern 包含实际的文件名,以及一个 %d 标识,这个标识的用法是 %d{},其中大括号里面是遵循 <code>java.text.SimpleDataFormat</code> 格式的日期模式,logback 会根据这个日期模式的最小单位来推断出是根据天、月、年还是分钟、小时来进行滚动的。<br>RollingFileAppender 有一个 file 属性,在配合 TimeBasedRollingPolicy 使用的时候,这个 file 的属性是否被设置,有着不同的区别。如果 file 属性设置了,那么每次新记录的日志将会记录到这个文件,当滚动时需要重命名的时候,则将这个文件名以 fileNamePattern 指定的模式进行重命名和归档。如果 file 属性没有进行设置,那么每次新纪录的日志会记录到根据 fileNamePattern 指定的当前日志文件里。<br>如果有多个 %d 标识,那么必须有一个是 primary 的,其它的为 auxiliary 的。例如 <code>/var/log/%d{yyyy/MM, aux}/myapplication.%d{yyyy-MM-dd}.log</code> 。同时,fileNamePattern 支持填写 Time Zone,例如 <code>aFolder/test.%d{yyyy-MM-dd-HH, UTC}.log</code>。</p><p>maxHistory 属性指定要保留多长时间的日志,依据从 fileNamePattern 中推断出来的单位。</p><p>totalSizeGap 属性限制了日志文件的大小总共不能超过的大小。</p><p>cleanHistoryOnStart 指定了是否重启应用的时候删除之前的归档日志。</p><p>以下是一些常见的 fileNamePattern,以及它们的作用。</p><table><thead><tr><th>fileNamePattern</th><th>Rollover schedule</th><th>Example</th></tr></thead><tbody><tr><td>/wombat/foo.%d</td><td>Daily rollover (at midnight). Due to the omission of the optional time and date pattern for the %d token specifier, the default pattern of yyyy-MM-dd is assumed, which corresponds to daily rollover.</td><td><strong>If the file property not set</strong>: During November 23rd, 2006, logging output will go to the file /wombat/foo.2006-11-23. At midnight and for the rest of the 24th, logging output will be directed to /wombat/foo.2006-11-24. <strong>If the file property set to /wombat/foo.txt</strong>: During November 23rd, 2006, logging output will go to the file /wombat/foo.txt. At midnight, foo.txt will be renamed as /wombat/foo.2006-11-23. A new /wombat/foo.txt file will be created and for the rest of November 24th logging output will be directed to foo.txt.</td></tr><tr><td>/wombat/%d{yyyy/MM}/foo.txt</td><td>Rollover at the beginning of each month.</td><td><strong>If the file property not set:</strong>During the month of October 2006, logging output will go to /wombat/2006/10/foo.txt. After midnight of October 31st and for the rest of November, logging output will be directed to /wombat/2006/11/foo.txt. <strong> If the file property set to /wombat/foo.txt:</strong>The active log file will always be /wombat/foo.txt. During the month of October 2006, logging output will go to /wombat/foo.txt. At midnight of October 31st, /wombat/foo.txt will be renamed as /wombat/2006/10/foo.txt. A new /wombat/foo.txt file will be created where logging output will go for the rest of November. At midnight of November 30th, /wombat/foo.txt will be renamed as /wombat/2006/11/foo.txt and so on.</td></tr><tr><td>/wombat/foo.%d{yyyy-ww}.log</td><td>Rollover at the first day of each week. Note that the first day of the week depends on the locale.</td><td>Similar to previous cases, except that rollover will occur at the beginning of every new week.</td></tr><tr><td>/wombat/foo%d{yyyy-MM-dd_HH}.log</td><td>Rollover at the top of each hour.</td><td>Similar to previous cases, except that rollover will occur at the top of every hour.</td></tr><tr><td>/wombat/foo%d{yyyy-MM-dd_HH-mm}.log</td><td>Rollover at the beginning of every minute.</td><td>Similar to previous cases, except that rollover will occur at the beginning of every minute.</td></tr><tr><td>/wombat/foo%d{yyyy-MM-dd_HH-mm, UTC}.log</td><td>Rollover at the beginning of every minute.</td><td>Similar to previous cases, except that file names will be expressed in UTC.</td></tr><tr><td>/foo/%d{yyyy-MM,aux}/%d.log</td><td>Rollover daily. Archives located under a folder containing year and month.</td><td>In this example, the first %d token is marked as auxiliary. The second %d token, with time and date pattern omitted, is then assumed to be primary. Thus, rollover will occur daily (default for %d) and the folder name will depend on the year and month. For example, during the month of November 2006, archived files will all placed under the /foo/2006-11/ folder, e.g /foo/2006-11/2006-11-14.log.</td></tr><tr><td>/wombat/foo.%d.gz</td><td>Daily rollover (at midnight) with automatic GZIP compression of the archived files.</td><td><strong> If the file property not set:</strong> During November 23rd, 2009, logging output will go to the file /wombat/foo.2009-11-23. However, at midnight that file will be compressed to become /wombat/foo.2009-11-23.gz. For the 24th of November, logging output will be directed to /wombat/folder/foo.2009-11-24 until it's rolled over at the beginning of the next day. <strong>If the file property set to /wombat/foo.txt:</strong> During November 23rd, 2009, logging output will go to the file /wombat/foo.txt. At midnight that file will be compressed and renamed as /wombat/foo.2009-11-23.gz. A new /wombat/foo.txt file will be created where logging output will go for the rest of November 24rd. At midnight November 24th, /wombat/foo.txt will be compressed and renamed as /wombat/foo.2009-11-24.gz and so on.</td></tr></tbody></table><p>配置示例:</p><pre><code><configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logFile.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- keep 30 days' worth of history capped at 3GB total size -->
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><h5>SizeAndTimeBasedRollingPolicy</h5><p>这个策略除了具有 TimeBasedRollingPolicy 的功能外,还能限制单个日志文件的大小,当单个日志到达指定的大小时,触发日志滚动。</p><pre><code><configuration>
<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>mylog.txt</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- rollover daily -->
<fileNamePattern>mylog-%d{yyyy-MM-dd}.%i.txt</fileNamePattern>
<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="ROLLING" />
</root>
</configuration></code></pre><p>通过 maxFileSize 来设置单个日志文件的大小限制。注意到这里,有一个新的标识出现 %i,这是当发生滚动时,可以为同一天的日志加上不同的后缀 index。</p><h5>FixedWindowRollingPolicy</h5><p>配置示例:</p><pre><code><configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>test.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>foo%i.log</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><p>以一个例子来认识 FixedWindowRollingPolicy</p><table><thead><tr><th>Number of rollovers</th><th>Active output target</th><th>Active log files</th><th>Description</th></tr></thead><tbody><tr><td>0</td><td>foo.log</td><td>-</td><td>No rollover has happened yet, logback logs into the initial file.</td></tr><tr><td>1</td><td>foo.log</td><td>foo1.log</td><td>First rollover. foo.log is renamed as foo1.log. A new foo.log file is created and becomes the active output target.</td></tr><tr><td>2</td><td>foo.log</td><td>foo1.log,foo2.log</td><td>Second rollover. foo1.log is renamed as foo2.log. foo.log is renamed as foo1.log. A new foo.log file is created and becomes the active output target.</td></tr><tr><td>3</td><td>foo.log</td><td>foo1.log, foo2.log, foo3.log</td><td>Third rollover. foo2.log is renamed as foo3.log. foo1.log is renamed as foo2.log. foo.log is renamed as foo1.log. A new foo.log file is created and becomes the active output target.</td></tr><tr><td>4</td><td>foo.log</td><td>foo1.log, foo2.log, foo3.log</td><td>In this and subsequent rounds, the rollover begins by deleting foo3.log. Other files are renamed by incrementing their index as shown in previous steps. In this and subsequent rollovers, there will be three archive logs and one active log file.</td></tr></tbody></table><h5>SizeBasedTriggeringPolicy</h5><p>配置示例,一般配合 FixedWindowRollingPolicy:</p><pre><code><configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>test.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>test.%i.log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration></code></pre><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
剖析 Druid SQL 解析器
https://segmentfault.com/a/1190000008120180
2017-01-14T15:08:04+08:00
2017-01-14T15:08:04+08:00
ytbean
https://segmentfault.com/u/ytbean
2
<p>原文链接:<a href="https://link.segmentfault.com/?enc=FNQywpCj4q%2Fyu5Jl2hncOA%3D%3D.Ksubf3l8ivc3EOVRfDF75P9%2FV0LoSpJedf%2FvfO%2FCkRYGOGsg8lJi8Dhz4UCof0B7" rel="nofollow">《剖析 Druid SQL 解析器》http://www.ytbean.com/posts/druid-sql-parser/</a></p><h2>认识 Druid</h2><p>Druid 是阿里巴巴公司开源的一个数据库连接池,它的口号是:<strong>为监控而生的数据库连接池</strong></p><p>根据<a href="https://link.segmentfault.com/?enc=WuA0M49UrdDJOJtTi%2FEvZw%3D%3D.47NcZ2xCpswTYH2Lz8c0ntbNDI8ft5o4pq7Av5bF%2BsUS7HxTRNjWz5RwzU%2FRPF%2By" rel="nofollow">官方 wiki</a>的介绍</p><blockquote>Druid 是一个 JDBC 组件库,包括数据库连接池、SQL Parser 等组件,DruidDataSource 是最好的数据库连接池。</blockquote><p>显然,官方有意无意地强调了 DruidDataSource 是最好的数据库连接池 -_- ...</p><h2>Druid SQL 解析器</h2><p>Druid 作为一个数据库连接池,功能很多,但我接触 Druid 的时候,却不是因为它有世界上最好的数据库连接池实现。而是因为有些开源项目(比如,mycat),借用了 Druid 的 SQL 解析功能。我需要研究这个开源项目,发现作为一个数据库中间件,它的 SQL 解析功能是直接引用的 Druid,Druid 包除了 SQL 解析模块的代码外,其它的代码并没有使用到。而这部分代码显然让人在研究 SQL 解析器代码时容易分心,产生厚重感和焦虑感。</p><p>Druid 本来的代码结构如下:</p><p><img src="/img/remote/1460000041527014" alt="image-20220310161603750" title="image-20220310161603750"></p><h2>提取 Druid SQL 解析器</h2><hr><p>在确认我并不需要使用到全世界最好的数据库连接池后,我想把除了 SQL 解析部分的代码全部剔除,仅仅留下 SQL 解析器模块。</p><p>一开始的做法当然是“暴力删除”,通过对代码的整体浏览,大概判断出哪些 package 与 SQL 解析有关,其余的直接删除。这样做会有些问题,比如说直接删除后在 IDE 中会立马浮现一些小红叉叉,但令人感到愉悦的是,Druid 的模块分解做得十分优秀,SQL 解析模块基本上作为一个工具模块,与其它模块实际上是分离的。</p><p>因此虽然是“暴力删除”,却也得到了一个令人满意的结果。</p><p>由于我只关注的是 Druid 对 MySQL 方言的解析,并且也不想看到 Druid 解析其它数据库方言的内容,也不愿被 Druid 那些为了适应多种数据库的“兼容性代码”混淆视听,因此狠下心来,把对其它 SQL 方言的支持也全都剔除,只留下与 MySQL 相关的代码。</p><p>剔除其它 SQL 方言并不是一个麻烦事,这也得益于 Druid 优秀的代码层次结构,基本上,只是拿类似以下形式的代码动刀</p><pre><code class="java">if type is oracle:
do something;
else if type is db2:
do something;
else if type is H2:
do something;
else if type is MySQL:
do something;</code></pre><p>把上面的代码,修改成:</p><pre><code>if type is MySQL:
do something;
else:
throw some exception;</code></pre><p>经过两层提取后,整个 Druid 就只剩下这些代码了</p><p><img src="/img/remote/1460000041527015" alt="image-20220310162346771" title="image-20220310162346771"></p><p>这是一个好的开始,着手研究并调试 SQL 解析器的代码也近在咫尺。</p><h2>解析器组成</h2><p>Druid 的<a href="https://link.segmentfault.com/?enc=NSsTwf99gqls3pSJLQBr2A%3D%3D.NabeDVs7PpAF5n4ch44rW%2FJIDo40LZeH7pC%2BqWPW3jsJTFWnJxeh7QyhzXf%2FMQV2" rel="nofollow">官方 wiki</a> 对 SQL 解析器部分的讲解内容并不多,但虽然不多,也有利于完全没接触过 Druid 的人对 SQL 解析器有个初步的印象。</p><p>说到解析器,脑海里便很容易浮现 <strong>parser</strong> 这个单词,然后便很容易联想到计算机科学中理论性比较强的学科------<strong>编译原理</strong>。想必很多人都知道(即使不知道,应该也耳濡目染)能够手写编译器的人并不多,并且这类人呢,理论知识和工程能力都比较强。在缺乏人力的条件下,大多数时候实现一个编译器,往往是选择采用一些工具,比如说 <strong>ANTLR</strong>,只需要描述好语法规则,这个工具就能生成对应的编译器。</p><p>不过,Druid 的 SQL 解析器是手写的,官方宣称性能是 ANTLR 这类工具的10倍以上。</p><p>在 Druid 的 SQL 解析器中,有三个重要的组成部分,它们分别是:</p><ul><li><p>Parser</p><ul><li>词法分析</li><li>语法分析</li></ul></li><li>AST(Abstract Syntax Tree,抽象语法树)</li><li>Visitor</li></ul><p>这三者的关系如下图所示:</p><p><img src="/img/remote/1460000041527016" alt="image-20220310162408256" title="image-20220310162408256"></p><p>Parser 由两部分组成,词法分析和语法分析。<br>当拿到一条形如 <code>select id, name from user</code> 的 SQL 语句后,首先需要解析出每个独立的单词,select,id,name,from,user。这一部分,称为<strong>词法分析</strong>,也叫作 <strong>Lexer</strong>。<br>通过词法分析后,便要进行语法分析了。<br>经常能听到很多人在调侃自己英文水平很一般时会说:<strong>26个字母我都知道,但是一组合在一起我就不知道是什么意思了</strong>。这说明他掌握了词法分析的技能,却没有掌握语法分析的技能。<br>那么对于 SQL 解析器来说呢,它不仅需要知道每个单词,而且要知道这些单词组合在一起后,表达了什么含义。语法分析的职责就是明确一个语句的语义,表达的是什意思。<br>自然语言和形式语言的一个重要区别是,自然语言的一个语句,可能有多重含义,而形式语言的一个语句,只能有一个语义;形式语言的语法是人为规定的,有了一定的语法规则,语法解析器就能根据语法规则,解析出一个语句的一个唯一含义。</p><p>AST 是 Parser 的产物,语句经过词法分析,语法分析后,它的结构需要以一种计算机能读懂的方式表达出来,最常用的就是抽象语法树。<br>树的概念很接近于一个语句结构的表示,一个语句,我们经常会对它这样看待:它由哪些部分组成?其中一个组成部分又有哪些部分组成?例如一条 select 语句,它由 select 列表、where 子句、排序字段、分组字段等组成,而 select 列表则由一个或多个 select 项组成,where 子句又由一个或者多个 where条件组成。<br>在我们人类的思维中,这种组成结构就是一个总分的逻辑结构,用树来表达,最合适不过。并且对于计算机来说,它显然比人类更擅长处理“树”。</p><p>AST 仅仅是语义的表示,但如何对这个语义进行表达,便需要去访问这棵 AST,看它到底表达什么含义。通常遍历语法树,使用 VISITOR 模式去遍历,从根节点开始遍历,一直到最后一个叶子节点,在遍历的过程中,便不断地收集信息到一个上下文中,整个遍历过程完成后,对这棵树所表达的语法含义,已经被保存到上下文了。有时候一次遍历还不够,需要二次遍历。遍历的方式,广度优先的遍历方式是最常见的。</p><h2>快速上手</h2><p>使用 Druid SQL Parser 来解析 SQL 语句,一般需要进行以下几个步骤:</p><ol><li>新建一个 Parser</li><li>使用 Parser 解析 SQL,生成 AST</li><li>使用 Visitor 访问 AST</li></ol><p>如下代码所示:</p><pre><code class="java">package io.beansoft.demo;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser;
import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlSchemaStatVisitor;
import com.alibaba.druid.sql.parser.SQLStatementParser;
/**
*
*
* @author beanlam
* @date 2017年1月10日 下午11:06:26
* @version 1.0
*
*/
public class ParserMain {
public static void main(String[] args) {
String sql = "select id,name from user";
// 新建 MySQL Parser
SQLStatementParser parser = new MySqlStatementParser(sql);
// 使用Parser解析生成AST,这里SQLStatement就是AST
SQLStatement statement = parser.parseStatement();
// 使用visitor来访问AST
MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
statement.accept(visitor);
// 从visitor中拿出你所关注的信息
System.out.println(visitor.getColumns());
}
}</code></pre><p>以上代码运行后控制台的输出为</p><pre><code>[user.id, user.name]</code></pre><p>当然,不使用 Visitor,直接操作 AST 也可以得到 SQL 语句的信息,在 Druid 现有内置的 Visitor 不能满足需求时,可以自己去实现 Visitor,对于用 Visitor 无法解析到的信息,可以直接访问 AST 去获取。</p><h2>Demo 代码</h2><p>以这份代码为例</p><pre><code class="java">/**
*
*
* @author beanlam
* @date 2017年1月10日 下午11:06:26
* @version 1.0
*
*/
public class ParserMain {
public static void main(String[] args) {
String sql = "select * from user order by id";
// 新建 MySQL Parser
SQLStatementParser parser = new MySqlStatementParser(sql);
// 使用Parser解析生成AST,这里SQLStatement就是AST
SQLStatement statement = parser.parseStatement();
// 使用visitor来访问AST
MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
statement.accept(visitor);
System.out.println(visitor.getColumns());
System.out.println(visitor.getOrderByColumns());
}
}</code></pre><p>一开始,需要初始化一个 Parser,在这里 <code>SQLStatementParser</code> 是一个父类,真正解析 SQL 语句的 Parser 实现是 <code>MySqlStatementParser</code>。<br>Parser 的解析结果是一个 <code>SQLStatement</code>,这是一个内部维护了树状逻辑结构的类。</p><h2>词法分析</h2><p>Druid 的代码里,代表<strong>语法分析</strong>和<strong>词法分析</strong>的类分别是 <code>SQLParser</code> 和 <code>Lexer</code>。并且, Parser 拥有一个 Lexer。</p><pre><code class="java">public class SQLParser {
protected final Lexer lexer;
protected String dbType;
public SQLParser(String sql, String dbType){
this(new Lexer(sql), dbType);
this.lexer.nextToken();
}
public SQLParser(String sql){
this(sql, null);
}
public SQLParser(Lexer lexer){
this(lexer, null);
}
public SQLParser(Lexer lexer, String dbType){
this.lexer = lexer;
this.dbType = dbType;
}
}</code></pre><p>经过瘦身后的 Druid 代码,其 Lexer 只有两个,分别是 <code>Lexer</code>,以及它的子类 <code>MySqlLexer</code><br>Lexer 作为词法分析器,必然拥有其词汇表,在Lexer里,以 <code>Keywords</code> 表示。</p><pre><code>protected Keywords keywods = Keywords.DEFAULT_KEYWORDS;</code></pre><p>Keywords 实际上是 key 为单词,value 为 Token 的字典型结构,其中 Token 是单词的类型,比如说,“select” 的 Token 类型就是 Select Token,而 “abc” 的 Token 类型,则是标识符,也表示为 Identifier Token。</p><p>而 <code>MySqlLexer</code> 类,除了沿用其父类的 Keywords 外,自己还有自己的 Keywords。可以理解为 Lexer 所维护的关键字集合,是通用的;而 MySqlLexer 除了有通用的关键字集合,也有属于 MySQL 数据库 SQL 方言的关键字集合。</p><p>Parser 是 Lexer 的使用者,站在 Parser 的角度看,它会怎么去使用 Lexer,或者说,Lexer 应该具备怎样的功能,才能满足 Parser 的使用需求。<br>Lexer 应该具备一个函数,能让使用者命令它解析一个单词,并且 Lexer 还必须提供一个函数,供使用者获取 Lexer 上一次解析到的单词以及单词的类型。<br>在 Lexer 中,<code>nextToken()</code> 这个方法提供了第一个需求,只要被调用,它就按顺序从 SQL 语句的开头到结尾,解析出下一个单词;<code>token()</code> 方法,则返回了上一次解析的单词的 Token 类型,如果 Token 类型是标识符(Identifier),Lexer 还提供了一个 <code>stringVal()</code> 方法,让使用者能拿到标识符的值。</p><p>走进 Lexer 的 <code>nextToken()</code> 方法,可以发现它的代码充斥着 <code>if</code> 语句和 <code>switch</code> 语句,因为解析单词的时候,是一个字符一个字符地解析,这就意味着,这个方法每次扫描一个字符,都必须判断单词是否结束,应该用什么方式来验证这个单词等等。这个过程,就是一个<strong>状态机</strong>运作的过程,每解析到一个字符,都要判断当前的状态,以决定应该进入下一个什么状态。</p><h2>Select 语法分析</h2><p>有了 Lexer 这样的犀利工具,接下来就是 Parser 发挥的时候了,从 Demo 代码里可以看到,解析的开始,在于调用 <code>parser.parseStatement()</code> 方法。进到这个方法看看,发现清一色是形似如下格式的代码:</p><pre><code class="java">if (lexer.token() == Token.xxx) {
// 这里解析 xxx 类型
return;
}
if (lexer.token() == Token.aaa) {
// 这里解析 aaa 类型
return;
}</code></pre><p>显然,如果是分析对 Select 类型的语句的解析,那么应该关注以下的代码:</p><pre><code class="java">if (lexer.token() == Token.SELECT) {
statementList.add(parseSelect());
continue;
}</code></pre><p>重点是 <code>parseSelect()</code> 方法,<code>MySqlStatementParser</code> 重载了它的父类的这个方法,因此这个方法实际上的实现细节是这样的</p><pre><code class="java"> @Override
public SQLStatement parseSelect() {
MySqlSelectParser selectParser = new MySqlSelectParser(this.exprParser);
SQLSelect select = selectParser.select();
if (selectParser.returningFlag) {
return selectParser.updateStmt;
}
return new SQLSelectStatement(select, JdbcConstants.MYSQL);
}</code></pre><p>初始化一个针对 MySQL Select 语句的 Parser,然后调用 <code>select()</code> 方法进行解析,把返回结果 <code>SQLSelect</code> 放到 <code>SQLSelectStatement</code> 里,而这个 <code>SQLSelectStatement</code>,便是我最关心的 AST 抽象语法树,SQLSelect 是它的第一个子节点。</p><p>抛开解析的细节不谈,实际上我会非常关心这棵 AST 的层次结构。</p><h2>Select 抽象语法树</h2><p>打开 <code>SQLSelectStatement</code> 的代码,扫描它的子成员,便分析出这样的一棵语法树:</p><p><img src="/img/remote/1460000041527017" alt="image-20220310162141483" title="image-20220310162141483"></p><p>这意味着,在 Druid 眼里,它是这样看待一条 Select 语句的所有成员部分的。</p><h2>Visitor</h2><p>从 demo 代码中可以看到,有了 AST 语法树后,则需要一个 visitor 来访问它</p><pre><code class="java"> // 使用visitor来访问AST
MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
statement.accept(visitor);
System.out.println(visitor.getColumns());
System.out.println(visitor.getOrderByColumns());</code></pre><p>statement 调用 accept 方法,以 visitor 作为参数,开始了访问之旅。在这里 statement 的实际类型是 <code>SQLSelectStatement</code>。</p><p>在 Druid 中,一条 SQL 语句中的元素,无论是高层次还是低层次的元素,都是一个 <code>SQLObject</code>,statement 是一种 SQLObject,表达式 expr 也是一种 SQLObject,函数、字段、条件等等,这些都是一种 SQLObject,SQLObject 是一个接口,<code>accept</code> 方法便是它定义的,目的是为了让访问者在访问 SQLObject 时,告知访问者一些事情,好让访问者在访问的过程中能够收集到关于该 SQLObject 的一些信息。</p><p>具体的 <code>accept()</code> 实现,在 <code>SQLObjectImpl</code> 这个类中,代码如下所示:</p><pre><code class="java"> public final void accept(SQLASTVisitor visitor) {
if (visitor == null) {
throw new IllegalArgumentException();
}
visitor.preVisit(this);
accept0(visitor);
visitor.postVisit(this);
}</code></pre><p>这是一个 final 方法,意味着所有的子类都要遵循这个模板,首先 accept 方法前和后,visitor 都会做一些工作。真正的访问流程定义在 <code>accept0()</code> 方法里,而它是一个<strong>抽象方法</strong>。</p><p>因此要知道 Druid 中是如何访问 AST 的,先拿 SQLSelectStatement 的 accept0() 方法来探探究竟。</p><pre><code class="java"> protected void accept0(SQLASTVisitor visitor) {
if (visitor.visit(this)) {
acceptChild(visitor, this.select);
}
visitor.endVisit(this);
}</code></pre><p>首先,使 visitor 访问自己,访问自己后,visitor 会决定是否还要访问自己的子元素。<br>打开 <code>MySqlSchemaStateVisitor</code> 的 visit 方法,可以看到,visitor 做了一些事,初始化了自己的 aliasMap,然后 return true,这意味着还要访问 SQLSelectStatement 的子节点。</p><pre><code class="java"> public boolean visit(SQLSelectStatement x) {
setAliasMap();
return true;
}</code></pre><p>接下来访问子元素</p><pre><code class="java"> protected final void acceptChild(SQLASTVisitor visitor, SQLObject child) {
if (child == null) {
return;
}
child.accept(visitor);
}</code></pre><p>由此可以看出,SQLObject 负责通知 visitor 要访问自己的哪些元素,而 visitor 则负责访问相应元素前,中,后三个过程的逻辑处理。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
JDBC 4.2 Specifications 中文翻译 -- 第六章 遵守规范
https://segmentfault.com/a/1190000008120136
2017-01-14T15:04:13+08:00
2017-01-14T15:04:13+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>本章指出了实现一个 JDBC 驱动所需要遵守的规范,在本章中没有指出的规范,则作为可选项来遵守。</p>
<h2>6.1 准则与要求</h2>
<p>以下的准则是 JDBC API 规范要求实现者遵守的基本准则</p>
<ul>
<li>JDBC API 的实现者必须支持 Entry Level SQL92 标准,以及 <code> Drop Table</code> 命令。对 Entry Level SQL92 标准的支持是实现 JDBC API 的最小要求,对于 SQL99 和 SQL2003 特性的实现,必须遵照 SQL99 和 SQL2003 的规范。</li>
<li>JDBC 驱动必须支持转义语法,转义语法在 第十三章 中有详细解释。</li>
<li>JDBC 驱动必须支持事务,参考 第十章。</li>
<li>如果 <code>DatabaseMetaData</code> 的某个方法指明某个特性的可用的,那么驱动必须根据这个特性的相关规范中规定的标准语法实现这个特性,如果该特性需要使用到数据源的原生 API 或者是 SQL 方言,那么由驱动负责实现从标准 SQL 语法到原生 API 或者 SQL 方言的映射关系。如果支持了某个特性,那么 <code>DatabaseMetaData</code> 中与这个特性相关的方法也必须提供实现。比如说,如果一个驱动实现了 <code>RowSet</code> 接口,那么它也应该实现 <code>RowSetMetaData</code> 接口。</li>
<li>驱动必须提供对底层数据源特性的访问方式,包括扩展了 JDBC API 的特性。这么规定的目的是能让使用了 JDBC API 的应用程度能像数据源的原生程序一样,访问与数据源有关的特性。</li>
<li>如果一个 JDBC 驱动不支持,或者部分支持某个可选的数据库特性,那么 <code>DatabaseMetaData</code> 的方法必须指明这个特性还没受到支持,任何还没实现或者还没支持的特性,如果应用程序使用到了,那么应该给应用程序抛一个 <code>SQLFeatureNotSupportedException</code>。</li>
</ul>
<blockquote>注意 —— 根据 SQL92 的规定, JDBC 驱动需要支持 <code>DROP TABLE</code> 命令,不过,是否实现 <code>CASCADE</code> 和 <code>RESTRICT</code>,则是可选的,不是必须的。此外, 当数据源里需要 <code>drop</code> 的表定义了视图、完整性约束时,如何实现 <code>DROP TABLE</code> 来处理这种情况,则每个驱动允许有不同的做法。</blockquote>
<h2>6.2 JDBC 4.2 API 要求驱动遵守的准则</h2>
<ul>
<li>上一节所述的所有准则</li>
<li>支持自动加载所有实现了 <code>java.sql.Driver</code> 的驱动类</li>
<li>
<code>ResultSet</code> 支持 <code>TYPE_FORWARD_ONLY</code> 类型</li>
<li>
<code>ResultSet</code> 支持 <code>CONCUR_READ_ONLY</code> 并发级别</li>
<li>支持批量更新</li>
<li>
<p>完全实现以下接口</p>
<ul>
<li>java.sql.DatabaseMetaData</li>
<li>java.sql.ParameterMetaData</li>
<li>java.sql.ResultSetMetaData</li>
<li>java.sql.Wrapper</li>
</ul>
</li>
<li>
<p>必须实现 <code>DataSource</code> 接口,但以下方法是可选的</p>
<ul><li>getParentLogger</li></ul>
</li>
<li>
<p>必须实现 <code>Driver</code> 接口,但以下方法是可选的</p>
<ul><li>getParentLogger</li></ul>
</li>
<li>
<p>必须实现 <code>Connection</code> 接口,但以下方法是可选的</p>
<ul>
<li>createArrayOf</li>
<li>createBlob</li>
<li>createClob</li>
<li>createNClob</li>
<li>createSQLXML</li>
<li>createStruct</li>
<li>getNetworkTimeout</li>
<li>getTypeMap</li>
<li>setTypeMap</li>
<li>prepareStatement(String sql, Statement.RETURN_GENERATED_KEYS)</li>
<li>prepareStatement(String sql, int[] columnIndexes)</li>
<li>prepareStatement(String sql, String[] columnNames)</li>
<li>setSavePoint</li>
<li>rollback(java.sql.SavePoint savepoint)</li>
<li>releaseSavePoint</li>
<li>setNetworkTimeout</li>
</ul>
</li>
<li>
<p>必须实现 <code>Statement</code> 接口,但以下方法是可选的</p>
<ul>
<li>cancel</li>
<li>execute(String sql, Statement.RETURN_GENERATED_KEYS)</li>
<li>execute(String sql, int[] columnIndexes)</li>
<li>execute(String sql, String[] columnNames)</li>
<li>executeUpdate(String sql, Statement.RETURN_GENERATED_KEYS)</li>
<li>executeUpdate(String sql, int[] columnIndexes)</li>
<li>executeUpdate(String sql, String[] columnNames)</li>
<li>getGeneratedKeys</li>
<li>getMoreResults(Statement.KEEP_CURRENT_RESULT),除非 DatabasemetaData.supportsMultipleOpenResults() 返回 true,否则是可选的。</li>
<li>getMoreResults(Statement.CLOSE_ALL_RESULTS) 除非 DatabasemetaData.supportsMultipleOpenResults() 返回 true,否则是可选的。</li>
<li>setCursorName</li>
</ul>
</li>
<li>
<p>必须实现 <code>PreparedStatement</code> 接口,但以下方法是可选的</p>
<ul>
<li>getMetaData</li>
<li>setArray, setBlob, setClob, setNClob, setNCharacterStream, setNString, setRef, setRowId, setSQLXML and setURL</li>
<li>setNull(int parameterIndex,int sqlType, String typeName)</li>
<li>setUnicodeStream</li>
<li>setAsciiStream, setBinaryStream, setCharacterStream,</li>
<li>setNCharacterStream</li>
</ul>
</li>
<li>
<p>如果 <code>DatabaseMetaData.supportsStoredProcedures()</code> 返回 true, 那么必须实现 <code>CallableStatement</code> 接口,但以下方法是可选的</p>
<ul>
<li>所有的 setXXX, getXXX 方法,以及所有支持命名参数的 registerOutputParameter 方法</li>
<li>getArray, getBlob, getClob, getNClob, getNCharacterStream, getNString, getRef, getRowId, getSQLXML and getURL</li>
<li>getBigDecimal(int parameterIndex,int scale)</li>
<li>getObject(int i, Class<T> type)</li>
<li>getObject(String colName, Class<T> type)</li>
<li>getObject(int parameterIndex, java.util.Map<java.lang.String,java.lang.Class<?>> map)</li>
<li>registerOutputParam(String parameterName,int sqlType, String typeName)</li>
<li>setNull(String parameterName,int sqlType, String typeName)</li>
<li>setAsciiStream, setBinaryStream, setCharacterStream, setNCharacterStream</li>
</ul>
</li>
<li>
<p>必须实现 <code>RowSet</code> 接口,但以下方法是可选的</p>
<ul>
<li>所有的 updateXXX 方法</li>
<li>absolute</li>
<li>afterLast</li>
<li>beforeFirst</li>
<li>cancelRowUpdates</li>
<li>deleteRow</li>
<li>first</li>
<li>getArray, getBlob, getClob, getNClob, getNCharacterStream, getNString, getRef, getRowId, getSQLXML and getURL</li>
<li>getBigDecimal(int i,int scale)</li>
<li>getBigDecimal(String colName,int scale)</li>
<li>getCursorName</li>
<li>getObject(int i, Class<T> type)</li>
<li>getObject(String colName, Class<T> type)</li>
<li>getObject(int i, Map<String,Class<?>> map)</li>
<li>getObject(String colName, Map<String,Class<?>> map)</li>
<li>getRow</li>
<li>getUnicodeStream</li>
<li>insertRow</li>
<li>isAfterLast</li>
<li>isBeforeFirst</li>
<li>isFirst</li>
<li>isLast</li>
<li>last</li>
<li>moveToCurrentRow</li>
<li>moveToInsertRow</li>
<li>previous</li>
<li>refreshRow</li>
<li>relative</li>
<li>rowDeleted</li>
<li>rowInserted</li>
<li>rowUpdated</li>
<li>updateRow</li>
</ul>
</li>
<li>
<p>如果一个 JDBC 驱动支持 <code>ResultSet</code> 的 <code>CONCUR_UPDATABLE</code> 并发级别,那么必须实现以下方法</p>
<ul>
<li>除了 updateArray, updateBlob, updateClob, updateNClob, updateNCharacterstream, updateNString, updateRef, updateRowId, updateSQLXML, updateURL, updateBlob, updateClob, updateNClob, updateAsciiStream, updateBinaryStream, updateCharacterStream and updateNCharacterstream 之外的所有 updateXXX 方法。</li>
<li>cancelRowUpdates</li>
<li>deleteRow</li>
<li>rowDeleted</li>
<li>rowUpdated</li>
<li>updateRow</li>
</ul>
</li>
<li>
<p>如果一个 JDBC 驱动支持 <code>TYPE_SCROLL_SENSITIVE</code> 和 <code>TYPE_SCROLL_INSENSITIVE</code> 类型的 <code>ResultSet</code>,那么必须实现以下方法</p>
<ul>
<li>absolute</li>
<li>afterLast</li>
<li>beforeFirst</li>
<li>first</li>
<li>isAfterLast</li>
<li>isBeforeFirst</li>
<li>isFirst</li>
<li>isLast</li>
<li>last</li>
<li>previous</li>
<li>relative</li>
</ul>
</li>
<li>
<p>如果一个可选实现的接口被实现了,那么这个接口的所有方法必须全部实现,除了以下的例外</p>
<ul><li>java.sql.SQLInput 和 java.sql.SQLOutput 接口不要求实现 Array, Blob, Clob, NClob, NString, Ref, RowId, SQLXML and URL 这些数据类型。</li></ul>
</li>
</ul>
<h2>6.3 Java EE 中的 JDBC 规范准则</h2>
<p>在 Java EE 环境中使用的 JDBC 驱动,除了必须遵守前文中提到所有规定外,还必须遵守以下规定:</p>
<ul><li>
<p>驱动必须支持存储过程,<code>DatabaseMetaData</code> 接口的 <code>supportsStoredProcedures</code> 方法必须返回 true,驱动也需要在调用 Statement, PreparedStatement, and CallableStatement 的方法时,支持转义语法,这些方法是:</p>
<ul>
<li>executeUpdate</li>
<li>executeQuery</li>
<li>execute</li>
</ul>
</li></ul>
<p>对于存储过程的支持,仅仅需要驱动在调用 Statement, PreparedStatement, and CallableStatement 接口的 execute 方法时,要么返回一个更新数量,要么返回一个单一的 ResultSet 对象。这是因为有些数据库不支持调用存储过程后返回多个 ResultSet 对象。</p>
<p>同时也要支持所有的参数类型,包括 IN, OUT, INOUT</p>
<ul><li>
<p>驱动必须支持下面这些函数的转义语法</p>
<ul>
<li>ABS</li>
<li>CONCAT</li>
<li>LCASE</li>
<li>LENGTH</li>
<li>LOCATE (two argument version only)</li>
<li>LTRIM</li>
<li>MOD</li>
<li>RTRIM</li>
<li>SQRT</li>
<li>SUBSTRING</li>
<li>UCASE</li>
</ul>
</li></ul>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
JDBC 4.2 Specifications 中文翻译 -- 第五章 类与接口
https://segmentfault.com/a/1190000008120106
2017-01-14T15:01:56+08:00
2017-01-14T15:01:56+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>以下的类和接口,组成了 JDBC API</p>
<h2>5.1 <code>java.sql</code>包</h2>
<p>JDBC API 的核心部分都藏在了 <code>java.sql</code> 这个包里,所有的枚举类,普通类以及接口,都在下方列了出来,其中,枚举和普通类是粗体,接口是正常字体。</p>
<p>java.sql.Array</p>
<p><strong>java.sql.BatchUpdateException</strong></p>
<p>java.sql.Blob</p>
<p>java.sql.CallableStatement</p>
<p>java.sql.Clob</p>
<p>java.sql.ClientinfoStatus</p>
<p>java.sql.Connection</p>
<p><strong>java.sql.DataTruncation</strong></p>
<p>java.sql.DatabaseMetaData</p>
<p><strong>java.sql.Date</strong></p>
<p>java.sql.Driver</p>
<p>java.sql.DriverAction</p>
<p><strong>java.sql.DriverManager</strong></p>
<p><strong>java.sql.DriverPropertyInfo</strong></p>
<p><strong>java.sql.JDBCType</strong></p>
<p>java.sql.NClob</p>
<p>java.sql.ParameterMetaData</p>
<p>java.sql.PreparedStatement</p>
<p><strong>java.sql.PseudoColumnUsage</strong></p>
<p>java.sql.Ref</p>
<p>java.sql.ResultSet</p>
<p>java.sql.ResultSetMetaData</p>
<p>java.sql.RowId</p>
<p><strong>java.sql.RowIdLifeTime</strong></p>
<p>java.sql.Savepoint</p>
<p><strong>java.sql.SQLClientInfoException</strong></p>
<p>java.sql.SQLData</p>
<p><strong>java.sql.SQLDataException</strong></p>
<p><strong>java.sql.SQLException</strong></p>
<p><strong>java.sql.SQLFeatureNotSupportedException</strong></p>
<p>java.sql.SQLInput</p>
<p><strong>java.sql.SQLIntegrityConstraintViolationException</strong></p>
<p><strong>java.sql.SQLInvalidAuthorizationSpecException</strong></p>
<p><strong>java.sql.SQLNonTransientConnectionException</strong></p>
<p><strong>java.sql.SQLNonTransientException</strong></p>
<p>java.sql.SQLOutput</p>
<p>java.sql.SQLPermission</p>
<p>java.sql.SQLSyntaxErrorException</p>
<p>java.sql.SQLTimeoutException</p>
<p>java.sql.SQLTransactionRollbackException</p>
<p>java.sql.SQLTransientConnectionException</p>
<p>java.sql.SQLTransientException</p>
<p>java.sql.SQLType</p>
<p>java.sql.SQLXML</p>
<p><strong>java.sql.SQLWarning</strong></p>
<p>java.sql.Statement</p>
<p>java.sql.Struct</p>
<p><strong>java.sql.Time</strong></p>
<p><strong>java.sql.Timestamp</strong></p>
<p><strong>java.sql.Types</strong></p>
<p>java.sql.Wrapper</p>
<p>下面这些类和接口是在 JDBC 4.2 API 中新增加或有过改动的,其中新增加的类和接口以粗体的形式表示</p>
<p>java.sql.BatchUpdateException</p>
<p>java.sql.CallableStatement</p>
<p>java.sql.Connection</p>
<p>java.sql.DatabaseMetaData</p>
<p>java.sql.Date</p>
<p>java.sql.Driver</p>
<p><strong>java.sql.DriverAction</strong></p>
<p>java.sql.DriverManager</p>
<p><strong>java.sql.JDBCType</strong></p>
<p>java.sql.Permission</p>
<p>java.sql.PreparedStatement</p>
<p>java.sql.ResultSet</p>
<p>java.sql.SQLInput</p>
<p>java.sql.SQLOutput</p>
<p><strong>java.sql.SQLType</strong></p>
<p>java.sql.SQLXML</p>
<p>java.sql.Statement</p>
<p>java.sql.Types</p>
<p>java.sql.Timestamp</p>
<p>javax.sql.XADataSource</p>
<p>下面这个图展示了 <code>java.sql</code> 包里关键的类和接口之间关系</p>
<p><img src="/classes-interfaces.jpg" alt="" title=""></p>
<h2>5.2 <code>javax.sql</code> 包</h2>
<p>下面列出来的是 <code>javax.sql</code> 这个包中的类和接口,类用粗体表示,接口是普通字体</p>
<p>javax.sql.CommonDataSource</p>
<p><strong>javax.sql.ConnectionEvent</strong></p>
<p>javax.sql.ConnectionEventListener</p>
<p>javax.sql.ConnectionPoolDataSource</p>
<p>javax.sql.DataSource</p>
<p>javax.sql.PooledConnection</p>
<p>javax.sql.RowSet</p>
<p><strong>javax.sql.RowSetEvent</strong></p>
<p>javax.sql.RowSetInternal</p>
<p>javax.sql.RowSetListener</p>
<p>javax.sql.RowSetMetaData</p>
<p>javax.sql.RowSetReader</p>
<p>javax.sql.RowSetWriter</p>
<p><strong>javax.sql.StatementEvent</strong></p>
<p>javax.sql.StatementEventListener</p>
<p>javax.sql.XAConnection</p>
<p>javax.sql.XADataSource</p>
<blockquote>注意 — <code>javax.sql</code> 这个包中的类和接口在 JDBC 2.0 API 中初次使用,在 J2SE 1.2 中,并没有包含这个包,这个包是作为 J2SE 1.2 平台的一个可选包。但在 J2SE 1.4 后,<code>javax.sql</code> 和 <code>java.sql</code> 一样,也成为了 Java 平台的一部分。</blockquote>
<p>以下的图展示了 <code>javax.sql.DataSource</code> 与 <code>java.sql.Connection</code> 的关系</p>
<p><img src="/img/bVIezj?w=419&h=232" alt="图片描述" title="图片描述"></p>
<p>下图展示了与连接池有关的组成部分</p>
<p><img src="/img/bVIezm?w=495&h=356" alt="图片描述" title="图片描述"></p>
<p>下图展示了分布式事务有关的组成部分</p>
<p><img src="/img/bVIezo?w=702&h=475" alt="图片描述" title="图片描述"></p>
<p>下图展示与 RowSet 有关的组成部分</p>
<p><img src="/img/bVIezr?w=726&h=550" alt="图片描述" title="图片描述"></p>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
JDBC 4.2 Specifications 中文翻译 -- 第四章 JDBC API 概览
https://segmentfault.com/a/1190000007989074
2017-01-02T23:45:40+08:00
2017-01-02T23:45:40+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>JDBC API 给 Java 程序提供了一种访问一个或者多个数据源的途径,在大多数情况下,数据源是关系型数据库,使用 SQL 语言来访问。但是,JDBC Driver 也可以实现为能够访问其它类型的数据源,比如说文件系统或面向对象的系统。 JDBC API 最主要的动机就是提供一种标准的 API ,让应用程序访问多种多样的数据源。</p>
<p>这一章介绍了 JDBC API 的一些关键概念,此外,也介绍 JDBC 程序的两种使用场景,分别是<strong>两层模型</strong>和<strong>三层模型</strong>,在不同的场景中,JDBC API 的功能是不一样的。</p>
<h2>4.1 建立连接</h2>
<p>JDBC API 定义了 <code>Connection</code> 接口来代表与某个数据源的一条连接。</p>
<p>典型情况下,JDBC 应用可以使用以下两种机制来与目标数据源建立连接</p>
<ul>
<li>
<strong><code>DriverManager</code></strong> — 这个类从 JDBC API 1.0 版本开始就有了,当应用程序第一次尝试去连接一个数据源时,它需要指定一个<strong>url</strong>,<code>DriverManager</code> 将会自动加载所有它能在 CLASSPATH 下找到的 JDBC 驱动(任何 JDBC API 4.0 版本前的驱动,需要手动去加载)。</li>
<li>
<strong><code>DataSource</code></strong> — 这个接口在 JDBC 2.0 Optionnal Package API 中首次被引进,更推荐使用 <code>DataSource</code>, 因为它允许关于底层数据源的具体信息对于应用来说是透明的。需要设置 <code>DataSource</code> 对象的一些属性,这样才能让它代表某个数据源。当这个接口的 <code>getConnection</code> 方法被调用时,这个方法会返回一条与数据源建立好的连接。应用程序可以通过改变 <code>DataSource</code> 对象的属性,从而让它指向不同的数据源,无须改动应用代码;同时 <code>DataSource</code> 接口的具体实现类也可以在不改动应用程序代码的情况下,进行改变。</li>
</ul>
<p>JDBC API 也对 <code>DataSource</code> 接口有两方面的扩展,目的是为了支持企业应用,这两个扩展的接口如下所示:</p>
<ul>
<li>
<strong><code>ConnectionPoolDataSource</code></strong> — 支持对物理连接的缓存和重用,这能提高应用的性能和可扩展性</li>
<li>
<strong><code>XADataSource</code></strong> — 使连接能在分布式事务中使用</li>
</ul>
<h2>4.2 执行 SQL 并操作结果集</h2>
<p>一旦建立好一个连接,应用程序便可以通过这条连接,调用响应的 API 来对底层的数据源执行查询或者更新操作, JDBC API 提供了对于 SQL2003 标准的实现的访问。由于不同的厂商对这个标准的支持程度不同,所以 JDBC API 提供了 <code>DatabaseMetadata</code> 这个接口,应用程序可以使用这个接口来查看某个特性是否受到底层数据库的支持。JDBC API 也定义了转义语法,允许应用程序去访问一些非标准的、某个数据库厂商独有的特性。使用转义语法能够让使用 JDBC API 的应用程序像原生应用程序一样去访问某些特性,并且也提高了应用的可移植性。</p>
<p>应用可以使用 <code>Connection</code> 接口中定义的方法,去指定事务的属性,并创建 <code>Statement</code> 对象、<code>PreparedStatement</code> 对象,或者 <code>CallableStatement</code> 对象。 这些 statement 用来执行 SQL 语句,并获取执行结果。<code>ResultSet</code> 接口包装一次 SQL 查询的结果。 statements 可以是批量的,应用能够在一次执行中,向数据库提交多条更新语句,作为一个执行单元。</p>
<p>JDBC API 的 <code>ResultSet</code> 接口扩展了 <code>RowSet</code> 接口,提供了一个功能更全面的对表格型数据进行封装和访问的容器。一个 <code>RowSet</code> 对象是一个 Java Bean 组件,在于底层数据源断开连接的情况下,也能对数据进行操作,比如说,一个 <code>RowSet</code> 对象可以被序列化,然后通过网络发送出去,这对于那些不想对表格型数据进行处理的客户端来说特别有用,并且无须在连接建立的情况下进行,就减轻了驱动程序的负担。<code>RowSet</code> 的另外一个特性是,它能够包含一个定制化的 reader,来对表格型数据进行访问,并非只能访问关系型数据库的数据。此外,一个 <code>RowSet</code> 对象,能在与数据源断开连接的情况下,对行数据进行改写,并且能够包含一个定制化的 writer,把改写后的数据写回底层的数据源。</p>
<h3>4.2.1 对 SQL 高级数据类型的支持</h3>
<p>JDBC API 定义了 SQL 数据类型到 JDBC 数据类型的相互转化规则,包括对 SQL2003 的高级数据类型的支持,比如说 <code>BLOB, CLOB, ARRAY, REF, STRUCT, XML, DISTINCT</code>。 JDBC 驱动的实现也可以定义个性化的转化规则(user-defined types, UDTS), 该用户定义的UDT能够映射到 Java 语言中的某个类。JDBC API 也提供了对外部数据的访问,比如说存储在文件里,但不受数据源管理的数据。</p>
<h2>4.3 两层模型</h2>
<p>两层模型定义了客户端层和服务端层,不同层实现不同的功能,如下图所示:</p>
<p><img src="/img/bVHGtN?w=463&h=295" alt="两层模型" title="两层模型"></p>
<p>客户端层包含应用程序以及一个或者多个 JDBC 驱动,这一层的主要职责是:</p>
<ul>
<li>表现层逻辑</li>
<li>业务逻辑</li>
<li>对于多语句事务或者分布式事务的事务管理</li>
<li>资源管理</li>
</ul>
<p>在这种模型中,应用程序直接与 JDBC 驱动交互,包括创建和管理物理连接,处理底层数据库的细节。应用程序可能会基于对底层数据源的类型的认知,去访问一些特有的、非标准的特性,以此来获得性能上的提升。</p>
<p>这个模型有一些缺点,如下所示:</p>
<ul>
<li>将表现层和业务层逻辑与底层的功能直接混合,这会使代码变得难以维护。</li>
<li>应用程序不具有可移植性,因为应用程序会使用到底层特定数据库的一些独有的特性,对于需要与多种数据源进行连接的应用程序来说,要特别注意不同厂商的数据库实现以及不同的特性。</li>
<li>限制了可扩展性。典型地,应用程序将会一直持久与数据库的连接,直到应用程序退出,这就限制了并发访问数据库的并发数,在这种模型中,所谓的性能、可扩展性以及可用性,需要 JDBC 驱动以及底层的数据库来共同保证。如果应用程序使用的 JDBC 驱动不止一种,那么情况就会更加复杂。</li>
</ul>
<h2>4.4 三层模型</h2>
<p>三层模型引进了一个中间层,来处理业务逻辑并作为基础设施,如下图所示</p>
<p><img src="/img/bVHGtV?w=695&h=529" alt="三层模型" title="三层模型"></p>
<p>这种架构对于企业应用来说,性能、可扩展性和可用性都得到提升,各层的职责如下所示:</p>
<ol>
<li>客户端层 — 仅作为表现层,只需要与中间层交互,而不须了解中间层的基础架构以及底层数据源的功能细节</li>
<li>
<p>中间层服务器 — 包含以下几个组成部分:</p>
<ul>
<li>实现了业务逻辑,并与客户端进行交互的应用程序。如果应用程序需要与底层数据源交互,它只需要关注高层次的抽象和逻辑连接,若不是底层的驱动 API。</li>
<li>为应用程序提供基础设施的应用服务器。这些基础设施包括对物理数据库连接的池化和管理、事务管理,以及对不同驱动 API 的不同点进行屏蔽,最后一点使得我们很容易写出可移植的应用程序,应用服务器这个角色可以由 Java EE 服务器来承担,应用服务器主要实现提供给应用程序使用的抽象层,并负责与 JDBC 驱动交互。</li>
<li>能够与底层数据源建立连接的 JDBC 驱动。每个驱动根据其底层数据源的特性,去实现标准的 JDBC API,驱动层可能会屏蔽掉 SQL2003 标准与数据源支持的 SQL 方言之间的不同。如果底层数据源并不是一个关系型的数据库,驱动需要去实现对应的关系层逻辑,提供给应用服务器使用。</li>
</ul>
</li>
<li>底层的数据源 — 这一层是数据所在的一层,可以包含关系型数据库,文件系统,面向对象数据库,数据仓库等等任何能组织和表现数据的东西,但它们都需要提供符合 JDBC API 规范的驱动。</li>
</ol>
<h2>4.5 JDBC 与 JavaEE 平台</h2>
<p>Java EE 组件,比如说 JavaServer Pages、Servlets以及 EJB 组件,通常都需要使用 JDBC API 来访问关系型的数据,当 Java EE 组件使用 JDBC API 时,它们的容器会管理事务以及数据源。这意味着 Java EE 组件的开发者不会直接使用 JDBC API 的事务管理和数据源管理的能力。更多内容,请参考 Java EE 平台规范。</p>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
JDBC 4.2 Specifications 中文翻译 -- 第三章 新特性
https://segmentfault.com/a/1190000007989027
2017-01-02T23:40:02+08:00
2017-01-02T23:40:02+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>JDBC API 4.2 规范在以下几个方面有所改动</p>
<h2>3.1 增加对 <code>REF CURSOR</code> 的支持</h2>
<p>有些数据库支持 <code>REF CURSOR</code> 数据类型,在调用存储过程后返回该类型的结果集。</p>
<h2>3.2 支持大数量的更新</h2>
<p>JDBC 当前的方法里返回一个更新数量时,返回的是一个 <code>int</code>,在某些场景下这会导致问题,因为数据集还在不停地增长。</p>
<h2>3.3 增加 <code>java.sql.DriverAction</code> 接口</h2>
<p>如果一个 driver 想要在它被 <code>DriverManager</code> 注销时得到通知,就要实现这个接口。</p>
<h2>3.4 增加 <code>java.sql.SQLType</code> 接口</h2>
<p>用来创建一个代表 SQL 类型的对象</p>
<h2>3.5 增加 <code>java.sql.JDBCType</code> 枚举类</h2>
<p>用来识别通用的 SQL 类型,目的是为了取代定义在 <code>Types.java</code> 类里的常量。</p>
<h2>3.6 增加 Java Object 类型与 JDBC 类型的映射(附录表B-4)</h2>
<p>增加 <code>java.time.LocalDate</code> 映射到 <code>JDBC DATE</code></p>
<p>增加 <code>java.time.LocalTime</code> 映射到 <code>JDBC TIME</code></p>
<p>增加 <code>java.time.LocalDateTime</code> 映射到 <code>JDBC TIMESTAMP</code></p>
<p>增加 <code>java.time.LocalOffsetTime</code> 映射到 <code>JDBC TIME_WITH_TIMEZONE</code></p>
<p>增加 <code>java.time.LocalOffsetDateTime</code> 映射到 <code>JDBC TIMESTAMP_WITH_TIMEZONE</code></p>
<h2>3.7 增加调用 <code>setObject</code> 和 <code>setNull</code> 方法时 Java 类型和 JDBC 类型的转换(附录表B-5)</h2>
<p>允许 <code>java.time.LocalDate</code> 转化为 <code>CHAR, VARCHAR, LONGVARCHAR, DATE</code></p>
<p>允许 <code>java.time.LocalTime</code> 转化为 <code>CHAR, VARCHAR, LONGVARCHAR, TIME</code></p>
<p>允许 <code>java.time.LocalTime</code> 转化为 <code>CHAR, VARCHAR, LONGVARCHAR, TIMESTAMP</code></p>
<p>允许 <code>java.time.OffsetTime</code> 转化为 <code>CHAR, VARCHAR, LONGVARCHAR, TIME_WITH_TIMESTAMP</code></p>
<p>允许 <code>java.time.OffsetDateTime</code> 转化为 <code>CHAR, VARCHAR, LONGVARCHAR, TIME_WITH_TIMESTAMP, TIMESTAMP_WITH_TIMESTAMP</code></p>
<h2>3.8 使用 <code>ResultSet</code> getter 方法来获得 JDBC 类型(附录表B-6)</h2>
<p>允许 <code>getObject</code> 方法返回 <code>TIME_WITH_TIMEZONE, TIMESTAMP_WITH_TIMEZONE</code></p>
<h2>3.9 JDBC API 的变化</h2>
<p>以下的 JDBC API 有了一些变化</p>
<h3>3.9.1 BatchUpdateException</h3>
<p>增加了一个新的构造函数来支持大量的 update,增加 <code>getLargeUpdateCounts</code> 方法。</p>
<h3>3.9.2 Connection</h3>
<p>增加了 <code>abort,getNetworkTimeout, getSchema, setNetworkTimeout, setSchema</code> 方法。<br>调整了 <code>getMapType, setSchema, setMapType</code> 方法。</p>
<h3>3.9.3 CallableStatement</h3>
<p>重载了 <code>registerOutParameter, setObject </code>方法。<br>调整了 <code>getObject</code> 方法</p>
<h3>3.9.4 Date</h3>
<p>增加了 <code>toInstant, toLocalDate</code> 方法。<br>重载了 <code>valueOf</code> 方法。</p>
<h3>3.9.5 DatabaseMetaData</h3>
<p>增加了 <code>supportsRefCursor, getMaxLogicalLobSize</code> 方法。<br>调整了 <code>getIndexInfo</code> 方法。</p>
<h3>3.9.6 Driver</h3>
<p>调整了 <code>acceptsURL, connect</code> 方法。</p>
<h3>3.9.7 DriverManager</h3>
<p>重载了 <code>registerDriver</code> 方法。<br>调整了 <code>getConnection, deregisterDriver, registerDriver</code> 方法。</p>
<h3>3.9.8 PreparedStatement</h3>
<p>增加了 <code>executeLargeUpdate</code> 方法。<br>重载了 <code>setObject</code> 方法。</p>
<h3>3.9.9 ResultSet</h3>
<p>重载了 <code>updateObject</code> 方法。<br>调整了 <code>getObject</code> 方法。</p>
<h3>3.9.10 Statement</h3>
<p>增加了 <code>executeLargeBatch, executeLargeUpdate,getLargeUpdateCount, getLargeMaxRows, setLargeMaxRows</code>方法。<br>调整了 <code>setEscapeProcessing</code> 方法。</p>
<h3>3.9.11 SQLInput</h3>
<p>增加了 <code>readObject</code> 方法</p>
<h3>3.9.12 SQLOutput</h3>
<p>增加了 <code>readObject</code> 方法</p>
<h3>3.9.13 Time</h3>
<p>增加了 <code>toInstant, toLocalTime</code> 方法<br>重载了 <code>valueOf</code> 方法</p>
<h3>3.9.14 Timestamp</h3>
<p>增加了 <code>from, toInstant, toLocalTime</code> 方法<br>重载了 <code>valueOf</code> 方法</p>
<h3>3.9.15 Types</h3>
<p>增加了 <code>REF_CURSOR, TIME_WITH_TIMEZONE, TIMESTAMP_WITH_TIEMZONE</code> 类型</p>
<h3>3.9.16 SQLXML</h3>
<p>调整了 <code>getSource setResult</code> 方法</p>
<h3>3.9.17 DataSource 与 XADataSource</h3>
<p>调整了必须提供一个无参构造函数</p>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
JDBC 4.2 Specifications 中文翻译 -- 第二章 目标
https://segmentfault.com/a/1190000007988982
2017-01-02T23:35:06+08:00
2017-01-02T23:35:06+08:00
ytbean
https://segmentfault.com/u/ytbean
1
<h2>2.1 JDBC API 的历史</h2>
<p>JDBC API 是一种成熟的技术,1997 年, 首次提出了 JDBC 规范,初始的版本中, JDBC API 仅提供一套针对数据库的调用级别接口。</p>
<p>从 2.0 版本和 2.1 版本开始,JDBC API 的功能得到增强,它能够支持更高级别的应用程序, 也提供一些高级特性,供开发应用服务器的开发者使用。</p>
<p>3.0 版本小范围地补充了一些遗漏的功能。</p>
<p>4.2 版本的目标主要有两个</p>
<ul>
<li>增强应用程序开发者对于 “简易开发” 的体验</li>
<li>增强企业应用级别的特性,提供一些工具集和 API 来更好地管理数据源</li>
</ul>
<h2>2.2 JDBC API 的目标</h2>
<p>下面列出了 JDBC API 的目标和设计理念</p>
<h3>2.2.1 与 Java EE 和 Java SE 平台无缝融合</h3>
<p>JDBC API 是 Java 平台的一部分,JDBC 4.2 API 应该跟随着 Java SE 平台和 Java EE 平台的大方向走,对于 Java 语言的新特性和改进,都应该也体现在 JDBC API 4.2 的规范中。</p>
<h3>2.2.2 符合 SQL:2003 标准</h3>
<p>JDBC API 提供了对标准SQL的对接,JDBC 3.0 致力于支持 SQL99 标准,对于 JDBC 4.2 来说,它的目标就是支持 SQL2003 标准</p>
<h3>2.2.3 提供与特定厂商实现无关的公有特性访问</h3>
<p>JDBC API 需要提供一种通用的特性访问形式,与具体厂商的实现无关。以前需要通过应用程序在上层去获得的特性,现在只需要通过 JDCB API 就能做到,不过,这对 API 的通用性和灵活性有较高的要求。</p>
<h3>2.2.4 保持对SQL的关注</h3>
<p>JDBC API 主要就是关注如何访问关系型的数据,这个目标从 3.0 版本以来,一直就是整个 JDBC API 规范的核心目标,JDBC API 规范将持续以<strong>易于使用</strong>为主题,对基于 SQL 的应用程序提供 API 和工具类的改进,不排除会使用其它的技术,比如说 XML、CORBA 和非关系型的数据源。</p>
<h3>2.2.5 作为高级层次 API 和工具的基础</h3>
<p>JDBC API 提供了一种标准的访问方式,能够应对大范围的数据源种类以及遗留系统,在 JDBC API 抽象层的隔离下,具体的 API 实现方式对使用者是透明的,对于那些想要开发出跨平台的工具和应用程序的厂商来说,JDBC API 相当于提供了一个非常有价值的平台。 JDBC API 可以作为构建高层次应用的基础设施,比如说 EJB,这都得益于 JDBC API 是提供了调用级别的接口。</p>
<h3>2.2.6 保持简单</h3>
<p>JDBC API 的宗旨就是容易使用,能成为构筑复杂程序的得力帮手,为了实现这个目标,JDBC API 定义了很多简洁高效,职责单一的接口方法,而不是在少量的方法里,通过增加一些flag类型的参数,使得一个方法变得臃肿,并且职责不单一。</p>
<h3>2.2.7 增强可靠性、可用性和可扩展性</h3>
<p>可靠性、可用性和可扩展性,这三个特性是 Java EE 平台和 Java SE 平台的主旨,也是未来 Java 平台的一个发展方向。JDBC API 通过增强资源管理、预编译语句的重用,以及错误处理来呼应这三个特性。</p>
<h3>2.2.8 保持对已有应用和驱动的向后兼容性</h3>
<p>已有的实现了 JDBC 4.2 的驱动,以及使用这些驱动的应用程序,必须继续运行在实现了 JDBC 4.2 规范的 Java 虚拟机中,而使用了旧版本的 JDBC 驱动和应用程序,无须改变它们的虚拟机运行环境,它们也可以直接地迁移到 JDBC 4.2 的环境中。</p>
<h3>2.2.9 解除与 JDBC RowSet 规范的关系</h3>
<p>Java SE 包含了一个标准的 JDBC RowSet 规范的实现,这个规范属于 JSR-114,该规范提供了一系列工具类层次和元数据语言层次的工具,允许开发者能够轻易地将使用到了 JDBC 技术的应用程序转变为 JDBC RowSet 模式的应用程序,这种模式允许存在不基于连接的数据访问,以及具有对数据源取自于 XML 的关系型数据的管理能力。</p>
<h3>2.2.10 允许前向兼容为 Connector 模式</h3>
<p>Connector 架构定义了一种打包和发布资源适配器的标准,这个标准使得 Java EE 容器能够将它自身的连接管理、事务管理,以及安全管理与外部数据源集成起来,JDBC API 为 JDBC 驱动转变为 Connector 架构提供了一个迁移路径。对于 JDBC 驱动的厂商来说, JDBC API 应该允许它们以增量的方式慢慢迁移到 Connector 架构,厂商需要写多一层资源管理包装器,这样就能把现有的驱动应用到 Connector 框架中。</p>
<h3>2.2.11 明确指定 JDBC 规定</h3>
<p>对于 JDBC 的规定,需要做到没有歧义,并且也容易辨认,JDBC API 规范以及 Javadoc 将会清楚地声明哪些规定的是必须要做到的,哪些是可选的。</p>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
JDBC 4.2 Specifications 中文翻译 -- 第一章 简介
https://segmentfault.com/a/1190000007988956
2017-01-02T23:32:20+08:00
2017-01-02T23:32:20+08:00
ytbean
https://segmentfault.com/u/ytbean
1
<h2>1.1 JDBC API 简介</h2>
<p>JDBC API 为 Java 语言提供了一种访问关系型数据库的方法。有了 JDBC API,应用程序便可以通过它来执行 SQL 语句、获取执行结果,以及对底层的数据库进行写入。JDBC API 也可以用来和多个数据源交互,这些数据源以分布式的形式部署。</p>
<p>JDBC API 基于 <strong>X/Open SQL CLI</strong> 标准,ODBC 也以它为标准。对于 <strong>X/Open SQL CLI</strong> 标准所定义的一些抽象概念,JDBC API 提供了对应的概念的映射,并且非常易用以及容易理解。</p>
<p>自从 1997 年 JDBC API 首次被提出以后, 它的接受程度原来越大,并且也出现了大量的对于 JDBC API 规范的实现,这都归功于 JDBC API 本身的灵活性。</p>
<h2>1.2 平台</h2>
<p>JDBC API 是 Java 语言的一部分,JDBC API 分为两个 package,分别是 <code>java.sql</code> 和 <code>javax.sql</code>。在 Java SE 平台和 Java EE 平台都存在这两个 package。</p>
<h2>1.3 目标读者</h2>
<ul>
<li>需要实现相应的驱动的数据库厂商</li>
<li>需要在 JDBC 驱动上建立多层服务的应用服务器厂商</li>
<li>需要基于 JDBC API 来开发应用的厂商</li>
</ul>
<p>本文档也适用于任何在 JDBC API 上层开发应用的开发者</p>
<h2>1.4 致谢</h2>
<blockquote>译者省略</blockquote>
<p><img src="/img/remote/1460000018839155" alt="扫一扫关注我的微信公众号" title="扫一扫关注我的微信公众号"></p>
Netty4.x Internal Logger 机制
https://segmentfault.com/a/1190000005797595
2016-06-25T11:02:53+08:00
2016-06-25T11:02:53+08:00
ytbean
https://segmentfault.com/u/ytbean
1
<p>原文链接:<a href="https://link.segmentfault.com/?enc=%2F94jchrp2bCRSwzjFyp5tA%3D%3D.fUkUPOpuDF7mEQRrzQ9VR0i5nWBVo%2FJYqvfp1BHctsldktzyYNxm2GPeqBqRFsmtsZ3%2FgcgL5xlTtUhIq2pPuQ%3D%3D" rel="nofollow">《Netty4.x Internal Logger 机制》http://www.ytbean.com/posts/netty-internal-logger/</a></p><p>Netty不像大多数框架,默认支持某一种日志实现。相反,Netty本身实现了一套日志机制,但这套日志机制并不会真正去打日志。相反,Netty自身的日志机制更像一个日志包装层。</p><h2>日志框架检测顺序</h2><p>Netty在启动的时候,会自动去检测当前Java进程的classpath下是否已经有其它的日志框架。<br>检查的顺序是:SLF4J -> Log4j -> jdk logging</p><p>先检查是否有slf4j,如果没有则检查是否有Log4j,如果上面两个都没有,则默认使用JDK自带的日志框架JDK Logging。<br>JDK的Logging就不用费事去检测了,直接拿来用了,因为它是JDK自带的。</p><blockquote>注意到虽然Netty支持Common Logging,但在Netty本文所用的4.10.Final版本的代码里,没有去检测Common Logging,即使有支持Common Logging的代码存在。</blockquote><h2>日志框架检测细节</h2><p>在Netty自身的代码里面,如果需要打日志,会通过以下代码来获得一个logger,以<code>io.netty.bootstrap.Bootstrap</code>这个类为例,读者可以翻开这个类瞧一瞧。</p><pre><code class="java">private static final InternalLogger logger = InternalLoggerFactory.getInstance(Bootstrap.class); </code></pre><p>要知道Netty是怎么得到logger的,关键就在于这个<code>InternalLoggerFactory</code>类了,可以看出来,所有的logger都是通过这个工厂类产生的。<br>翻开<code>InternalLoggerFactory</code>类的代码,可以看到类中有一个静态初始化块</p><pre><code class="java"> private static volatile InternalLoggerFactory defaultFactory;
static {
final String name = InternalLoggerFactory.class.getName();
InternalLoggerFactory f;
try {
f = new Slf4JLoggerFactory(true);
f.newInstance(name).debug("Using SLF4J as the default logging framework");
defaultFactory = f;
} catch (Throwable t1) {
try {
f = new Log4JLoggerFactory();
f.newInstance(name).debug("Using Log4J as the default logging framework");
} catch (Throwable t2) {
f = new JdkLoggerFactory();
f.newInstance(name).debug("Using java.util.logging as the default logging framework");
}
}
defaultFactory = f;
} </code></pre><p>Javaer们都知道,类的初始化块会在类<strong>第一次被使用</strong>的时候执行。那么什么时候称之为<strong>第一次被使用</strong>呢?比如说,静态方法被调用,静态变量被访问,或者调用构造函数。<br>当调用<code>InternalLoggerFactory.getInstance(Bootstrap.class)</code><strong>之前</strong>,上面的静态块会被调用,而Netty对于当前应用所使用的日志框架的检测,就是在这短短的20几行代码里面实现。</p><p>首先从代码整体上可以看到,一个<code>try-catch</code>,在<code>catch</code>里面又嵌套了一个<code>try-catch</code>,这正好体现了日志框架的检测顺序:<strong>先检测SLF4J,后检测Log4J,都没有的话,就直接使用JDK Logging</strong></p><h3>检测SLF4J</h3><p>在<code>f = new Slf4JLoggerFactory(true);</code>这里开始检测SLF4J是否存在。</p><pre><code class="java">public class Slf4JLoggerFactory extends InternalLoggerFactory {
public Slf4JLoggerFactory() {
}
Slf4JLoggerFactory(boolean failIfNOP) {
assert failIfNOP; // Should be always called with true.
// SFL4J writes it error messages to System.err. Capture them so that the user does not see such a message on
// the console during automatic detection.
final StringBuffer buf = new StringBuffer();
final PrintStream err = System.err;
try {
System.setErr(new PrintStream(new OutputStream() {
@Override
public void write(int b) {
buf.append((char) b);
}
}, true, "US-ASCII"));
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
try {
if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) {
throw new NoClassDefFoundError(buf.toString());
} else {
err.print(buf.toString());
err.flush();
}
} finally {
System.setErr(err);
}
}
@Override
public InternalLogger newInstance(String name) {
return new Slf4JLogger(LoggerFactory.getLogger(name));
}
} </code></pre><p>在这里可以看到<code>Slf4JLoggerFactory</code>是<code>InternalLoggerFactory</code>的一个子类实现。<br>如果应用的classpath下存在slf4j相关的jar包,那么当slf4j的日志框架初始化的时候,如果产生了什么错误,将会通过<code>System.err</code>输出;<br>对于Netty来讲,即使slf4j初始化失败,它也不愿让用户看到错误输出,因为对netty来说,slf4j初始化失败并不代表netty不能选择其它日志框架;<br>所以可以从上面代码中看到,一开始先把<code>System.err</code>给替换掉,让err输出被重定向到一个<code>StringBuffer</code>,如下代码所示:</p><pre><code class="java"> // SFL4J writes it error messages to System.err. Capture them so that the user does not see such a message on
// the console during automatic detection.
final StringBuffer buf = new StringBuffer();
final PrintStream err = System.err;
try {
System.setErr(new PrintStream(new OutputStream() {
@Override
public void write(int b) {
buf.append((char) b);
}
}, true, "US-ASCII"));
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}</code></pre><p>我们已经明白上面这段代码,就是为了重定向err输出,不让用户轻易看到。接下来看这些代码:</p><pre><code class="java"> try {
if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) {
throw new NoClassDefFoundError(buf.toString());
} else {
err.print(buf.toString());
err.flush();
}
} finally {
System.setErr(err);
}</code></pre><p>首先可以看到一个<code>try-finally</code>结构,finally块里把<code>System.err</code>复位了,也就是说在初始化SLF4J之后,无论发生什么事,都应该把System.err复位。<br>接下来看try块里面的代码:</p><pre><code class="java"> if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) {
throw new NoClassDefFoundError(buf.toString());
} else {
err.print(buf.toString());
err.flush();
}</code></pre><p>解释这些代码之前,我们先要认识到,SLF4J其实是一个日志门面(facade),它可以充当Log4j, Logback等日志框架的包装器。因此你的应用除了要有slf4j的依赖包,还要有其它具体的日志实现框架的依赖。例如下面是我的maven依赖,依赖了slf4j还有Logback。</p><pre><code> <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency> </code></pre><p>如果只有slf4j,而没有logback,那么<code>LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory</code>就会为true,然后代码就会抛出<code>NoClassDefFoundError</code>。<br>如果连slf4j本身都没有呢?那么运行到<code>LoggerFactory.getLoggerFactory()</code>就已经抛出异常了,因为找不到这个<code>LoggerFactory</code>类。<br>以上便是检测SLF4J的整个过程。</p><h3>检测Log4J</h3><p>如果需要检测Log4J,则说明检测不到SLF4J的存在,或者是SLF4J不可以使用。<br>回到<code>InternalLoggerFactory</code>代码里:</p><pre><code class="java"> try {
f = new Slf4JLoggerFactory(true);
f.newInstance(name).debug("Using SLF4J as the default logging framework");
defaultFactory = f;
} catch (Throwable t1) {
try {
f = new Log4JLoggerFactory();
f.newInstance(name).debug("Using Log4J as the default logging framework");
} catch (Throwable t2) {
f = new JdkLoggerFactory();
f.newInstance(name).debug("Using java.util.logging as the default logging framework");
}
} </code></pre><p>Log4J的检测很简单,很直接,直接在<code>newInstance()</code>方法里加载<code>org.apache.log4j.Logger;</code>类,如果加载不到,直接抛异常,然后转而直接使用JDK Logging。</p><pre><code class="java">public class Log4JLoggerFactory extends InternalLoggerFactory {
@Override
public InternalLogger newInstance(String name) {
return new Log4JLogger(Logger.getLogger(name));
}
} </code></pre><h2>兼容性</h2><h3>日志级别</h3><p>Netty的内部日志机制也自定义了日志打印级别,像日志的layout或者appender,则没有自己定义,完全交给底层的日志框架去做。</p><pre><code class="java">public enum InternalLogLevel {
/**
* 'TRACE' log level.
*/
TRACE,
/**
* 'DEBUG' log level.
*/
DEBUG,
/**
* 'INFO' log level.
*/
INFO,
/**
* 'WARN' log level.
*/
WARN,
/**
* 'ERROR' log level.
*/
ERROR
} </code></pre><p>这里会面临日志打印级别的兼容性问题,因为SLF4J,Log4J,以及JDK Logging,都有自己的日志打印级别,比如说JDK Logging,它的日志打印级别是这样的:</p><ul><li>SEVERE (highest value)</li><li>WARNING</li><li>INFO</li><li>CONFIG</li><li>FINE</li><li>FINER</li><li>FINEST (lowest value)</li></ul><p>不仅数目对不上,而且名称也没对上。Netty采用的方式是,按级别的高低来匹配,比如Netty的DEBUG将会对应到JDK的FINE,以此做到级别的对应关系和兼容性。</p><h3>消息格式化</h3><p>SLF4J的Logger会有这样一种打日志的方式,采用<strong>占位</strong>的方式,举个例子:、</p><pre><code>logger.info("Hello, I m {}, I m the president of {}","Obama","America");</code></pre><p>上面这行代码的日志输出结果是:</p><pre><code>Hello, I m Obama, I m the president of America</code></pre><p>可以看到,<strong>{}</strong>大括号是一个占位符,其内容将会被后面的参数所代替。<br>但Log4J和JDK Logging并不支持这种占位的日志打印方式,因此Netty又自己搞了一下,让它的<code>InternalLogger</code>可以以占位的方式格式化日志输出信息。<br>详情可以参考<code>io.netty.util.internal.logging.MessageFormatter</code>这个类,到这里就不再展开了。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
ZooKeeper 安装部署与配置
https://segmentfault.com/a/1190000005627489
2016-06-03T01:51:51+08:00
2016-06-03T01:51:51+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>原文链接:<a href="https://link.segmentfault.com/?enc=V9MKKT1h1Qea%2B0mkcKt9Sw%3D%3D.lkJMRHZdNhMLJAhpebPvfU%2FnU6dFldfbNpA9WT2W4QiLt8W%2BW7qnFxT5EvelEfu1%2B8iir%2BIJxyH9V3ebINz5WA%3D%3D" rel="nofollow">《ZooKeeper 安装部署与配置》http://www.ytbean.com/posts/zookeeper-installation/</a></p><h2>单实例安装</h2><h3>下载</h3><p>zookeeper使用Java编写,依赖的JDK版本为1.6+,因此zookeeper的运行需要Java环境的支持。<br>可以在以下网站下载到zookeeper的安装包<br><a href="https://link.segmentfault.com/?enc=TeimRteF3yEaUeDcUVPBRg%3D%3D.4FFN2dIw0hMoova5HmKUrnPPg%2FGAAwE8Wd9XCAqrlVQzUvMsJwOXLvsirueJBLaF" rel="nofollow">apache镜像</a><br>下载到的安装包为:zookeeper-3.4.8.tar.gz</p><p>在linux下用tar命令解压</p><pre><code>tar zxvf zookeeper-3.4.8.tar.gz</code></pre><p><img src="/img/remote/1460000041528376" alt="image-20220310181423639" title="image-20220310181423639"></p><p>其中bin目录包含了一些脚本,用来运行zookeeper的主程序、客户端,以及环境变量配置的脚本。<br>conf目录,见名思义,放置了配置文件。<br>lib目录,包含了zookeeper运行时所需要依赖的jar包。</p><h3>运行一下</h3><p>conf目录下包含了一个文件<strong>zoo_sample.cfg</strong>,然而它并不是真正的配置文件,只是一个配置文件的范例。<br>启动zk server的时候,如果不显式指定启动时的配置文件,默认为使用conf目录下的<strong>zoo.cfg</strong>文件作为配置文件。<br>可是conf目录下并没有啊,因此首先要把文件改下名:</p><pre><code>mv conf/zoo_sample.cfg conf/zoo.cfg</code></pre><p>接下来切换到<strong>bin</strong>目录下,启动zookeeper</p><pre><code>./zkServer.sh start</code></pre><p><img src="/img/remote/1460000041528377" alt="image-20220310181442950" title="image-20220310181442950"></p><p>有时候我觉得每次都要切换到bin目录,然后敲命令,挺麻烦的。所以会做一些alias,爱护手指,减少打字。编辑自己home目录下的<strong>.bash_profile</strong>文件,加上</p><p><img src="/img/remote/1460000041528378" alt="image-20220310181514740" title="image-20220310181514740"></p><p>然后source一下,让它立即生效</p><p><img src="/img/remote/1460000041528379" alt="image-20220310181556802" title="image-20220310181556802"></p><p>注意到这次启动和上一次动的输出不一样,明显输出的信息多了很多,原因是在启动zk的时候用的是<code>zkServer.sh start-foreground</code>。<br>这种启动方式让zk以前台进程的方式运行,这种运行方式也会占用屏幕。不过在调试的时候,我们还是挺需要这些输出信息的,有时候能帮我们做出一些基本的判断。</p><p><strong>zkServer.sh</strong>这个脚本支持多种参数</p><p><img src="/img/remote/1460000041528380" alt="image-20220310181624407" title="image-20220310181624407"></p><p>可以看到Usage这一行,包括了所有支持的参数。</p><h3>客户端连接</h3><p><strong>bin</strong>目录下包含有一个名为<strong>zkCli.sh</strong>的脚本,zookeeper也支持其它客户端。例如Java的有<code>curator</code>等。<br>但这个脚本作为我们尝试zookeeper的客户端,也是挺便利的。<br>再启动zk后,另开一个会话窗口,启动客户端去连接zk。</p><p><img src="/img/remote/1460000041528381" alt="image-20220310181641013" title="image-20220310181641013"></p><p>从输出的信息中可以看到客户端运行的环境,比如jdk的版本,classpath<br>这里值得注意的是最后有一个watcher相关的信息,每当客户端与zk服务器建立连接时,服务端都会发送一个状态为<strong>SyncConnected</strong>的watch event给客户端。客户端可以基于此,做出一些相应的动作。由于我们用的是这样的脚本形式的客户段,还没能对这个event进行处理。<br>zookeeper自带的客户端以及一些其他的Java客户端例如curator都能对这种事件进行处理。</p><h2>伪集群安装</h2><p>只有一台linux主机,但却想要搭建一套zookeeper集群的环境。<br>可以使用伪集群模式来搭建。<br>伪集群模式本质上就是在一个linux操作系统里面启动多个zookeeper实例。<br>这些不同的实例使用不同的端口,配置文件以及数据目录。</p><h3>创建独立的目录</h3><p>创建三个目录,隔离开3个zookeeper实例的数据文件,配置文件:</p><pre><code>[beanlam@localhost ~]$ mkdir zk1
[beanlam@localhost ~]$ mkdir zk2
[beanlam@localhost ~]$ mkdir zk3</code></pre><p>然后,再分别为每个目录创建一个数据目录,用来存放数据以及id文件</p><pre><code>[beanlam@localhost ~]$ mkdir zk1/data
[beanlam@localhost ~]$ mkdir zk2/data
[beanlam@localhost ~]$ mkdir zk3/data</code></pre><h3>指定id</h3><p>zookeeper启动的时候,会在它的数据目录下寻找id文件,以便知道它自己在集群中的编号。</p><pre><code>[beanlam@localhost ~]$ echo 1 > zk1/data/myid
[beanlam@localhost ~]$ echo 2 > zk2/data/myid
[beanlam@localhost ~]$ echo 3 > zk3/data/myid</code></pre><h3>修改配置文件</h3><p>这3个实例,每个实例都会使用不同的配置文件启动。<br>配置示例如下:</p><pre><code># The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/home/beanlam/zk1/data
# the port at which the clients will connect
clientPort=2181
server.1=127.0.0.1:2222:2223
server.2=127.0.0.1:3333:3334
server.3=127.0.0.1:4444:4445</code></pre><p>这是第一个实例的配置,z1.cfg。把这份配置文件放置在zk1/目录下。<br>同理,第二个和第三个实例的配置分别为z2.cfg和z3.cfg。和第一个实例一样,放在相同的位置。<br>唯一不同的是,clientPort必须修改一下,z1.cfg为2181,z2.cfg和z3.cfg不能也是2181,必须彼此不同,比如2182或者2183。</p><p>配置文件最底下有一个server.n的配置项,这里配置了两个端口,却一种第一个用于集群间实例的通信,第二个用于leader选举。<br>至于2181,用于监听客户端的连接。</p><h3>启动和连接</h3><p>按照以下方式,依次启动3个实例:</p><pre><code>[beanlam@localhost ~]$ cd zk1
[beanlam@localhost zk1]$ ~/zookeeper-3.4.8/bin/zkServer.sh start-foreground ./z1.cfg</code></pre><p>启动第一个和第二个实例的时候会有报错信息,因为其它实例还没启动完全,连接无法建立的原因,可以直接忽略。<br>启动完3个实例后,会发现其中有一个是leader,另外两个是follower。可观察输出信息。</p><p>接下来启动一个客户端去进行连接:</p><pre><code>[beanlam@localhost ~]$ ~/zookeeper-3.4.8/bin/zkCli.sh -server 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183</code></pre><p>可以看到,客户端连接上了刚才启动的三个实例中的其中一个。</p><h2>集群中实例的数量</h2><p>应用程序通过zookeeper客户端连接zookeeper。<br>客户端可以是zookeeper自身携带的客户端(zookeeper把client代码跟server放在一起,这点很多人有非议)<br>也可以是一些其它的开源客户端例如apache curator和zkClient。</p><p><img src="/img/remote/1460000041528382" alt="image-20220310181907886" title="image-20220310181907886"></p><p>zookeeper可以有两种部署模式,一种是单机版,一种是集群版。<br>所谓单机版,亦即只有一个zookeeper实例。<br>集群版会有多个zookeeper实例,多个实例之间会有一个master,他们之间的状态信息也会进行复制。</p><h3>集群的数目:奇数</h3><p>从各种书或网上的资料上经常能看到一个建议:</p><blockquote>zookeeper的集群里,实例的个数最好是奇数</blockquote><p>对于客户端来说,它并不需要关心zookeeper集群中有多少个实例,实例之间是怎么协商的。<br>如果客户端新建了一个znode,并得到了集群的响应,那么客户端就可以认为集群已经替它保存好了这个znode。<br>而实际上,在zookeeper集群内部,它们并不会把znode的创建都通知到每个zookeeper实例后才返回响应消息给客户端。</p><p>可以看出来,在zookeeper集群中,有一件非常重要的事经常会发生,那就是如何让集群中的每个实例对某个公共状态的变化达成共识。<br>公共状态的变化包括什么呢?</p><ul><li>leader选举</li><li>来自客户端的各种updates</li></ul><p>zookeeper采用的方式是,如果集群中有<strong>过半数</strong>的实例同意某个公共状态的变化,那么便认为集群最终会对这个公共状态达成一致意见。</p><h3>为什么是<strong>过半数</strong>?</h3><p><strong>我们先看看如果不是“过半数”,会发生什么情况:</strong><br>假设现在集群中有5个实例,当客户端新建一个znode “/test”后,只有2个实例同步了这个新建的znode,并通知客户端znode创建成功了。<br>在这2个实例通知其它3个实例之前,与其他3个实例发生了网络隔离(俗称“脑裂”,split-brain),变成了两个小集群了。<br>有2个实例拥有/test这个znode,而另外3个没有/test这个znode,并且在网络隔离还没恢复之前,这3个没有znode的实例永远不会得到关于/test这个znode被创建的通知。<br>当有一个客户端碰巧连接到的是那3个实例中的其中一个时,客户端永远也看不到/test这个znode。<br>这就出现了数据不一致的情况了。<br><strong>如果是“过半数”,那情况就不一样了:</strong><br>还是假设集群中有5个实例,这个时候,必须得有至少3个实例同步了/test这个znode后,客户端才会得到响应。<br>再次考虑网络隔离发生的场景,在这个3个实例还没通知到另外2个实例之前,又被隔离成两个小集群。<br>假设客户端连接到的是2个实例的那个集群,由于zookeeper认为要至少3个实例(过半数)存活才能提供服务,所以客户端获取不成功,数据是一致的。</p><blockquote>只要是采取“过半数”的策略,无论网络怎么隔离,无论脑怎么裂,能够提供服务(意味着有过半数的实例)的那个小集群里,至少有一个实例是同步到最新的状态信息的。</blockquote><p>包括zookeeper本身的leader选举,以及对znode的更新操作,都需要“过半数“这个作为基本方针。</p><h3>为什么是奇数</h3><p>偶数也可以有<strong>过半数</strong>,例如,4个实例的集群,3就是一个<strong>过半数</strong>。<br><strong>但为什么还是奇数最好?</strong><br>如果集群有5个实例,那么只能容忍2个实例的崩溃。<br>如果集群中有6个实例,同样也只能容忍2个实例的崩溃。<br>在相同的容忍度下,6个和5个有什么区别:</p><ul><li>由于集群间需要互相通信,实例越多,网络开销越大</li><li>实例越多,5个实例的时候,发生3个实例崩溃的概率要小于6个实例的时候。</li></ul><p><strong>那我们为什么不比较6个和7个?</strong><br>没有可比性,容忍度不同。</p><p><strong>如果有3,5,7供选择,该怎么选?</strong><br>集群中的实例数目越多,就越稳定。<br>但实例数目越多,网络开销以及实例之间协调的耗费也会比较大。</p><blockquote>这只是一个权衡利弊取其轻的原则</blockquote><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
MySQL Replication 配置与原理
https://segmentfault.com/a/1190000004602717
2016-03-14T16:25:09+08:00
2016-03-14T16:25:09+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>原文链接:<a href="https://link.segmentfault.com/?enc=kFDEZVP%2BFOuJ%2BpBXoIvI4Q%3D%3D.BEtp3xn5RW18sfzsddPddMxnlWpgifQL%2FaHsTcdQWRkmLvFqmiRLho0sshXbt6r8" rel="nofollow">《MySQL Replication 配置与原理》http://www.ytbean.com/posts/mysql-replication/</a></p><h2>总体架构</h2><h3>内部架构</h3><p><img src="/img/remote/1460000041528233" alt="image-20220310175129529" title="image-20220310175129529"></p><h4>连接管理</h4><p>MySQL服务端采用线程池维护客户端连接</p><h4>SQL解析</h4><p>分析查询语句,生成解析树,并将解析结果放入缓存中</p><h4>优化器</h4><p>优化包括选择合适的索引,数据的读取方式,分析语句执行的开销以及统计信息,优化器可以和存储引擎直接交互,尽管看起来似乎不必要。</p><h4>执行</h4><p>执行查询语句,返回结果</p><h4>缓存</h4><p>缓存SQL解析器解析后的结果</p><h4>存储引擎</h4><h5>锁</h5><ol><li><p>粒度</p><ul><li>表级锁(table lock) : 资源消耗最少,但并发度低</li><li>行级锁(row lock) : 资源消耗较多,但并发度高</li></ul></li><li><p>死锁<br> 死锁的简单例子:两个transaction同时开始执行,它们分别开始执行第一条update的语句时,便锁住了对方的资源,你拿着我的资源不放,我拿着你的资源不放,最后二人都僵持着,当事人事不关己高高挂起,旁观者疾呼:死锁!</p><pre><code class="sql"># transaction 1
START TRANSACTION;
UPDATE USER SET NAME='Bob' WHERE ID=1;
# sleep for some time;
UPDATE USER SET NAME='Jack' WHERE ID=2;
COMMIT;</code></pre><pre><code class="sql"># transaction 2
START TRANSACTION;
UPDATE USER SET NAME='Bob' WHERE ID=2;
# sleep for some time
UPDATE USER SET NAME='Jack' WHERE ID=1;
COMMIT;</code></pre></li><li>显示锁定和隐式锁定<br>隐式锁定就是系统自动加锁而不是人为的添加锁,显示锁定就是人为的添加锁,比如lock tables或者unlock tables。</li></ol><h5>事务</h5><p>事务的特点由存储引擎决定,是MySQL与其它数据库的不同。<br>不支持事务的存储引擎有:</p><ul><li>MYISAM</li><li>MEMORY</li><li>ARCHIEVE</li></ul><p>MySQL中的表按是否支持事务,分为:事务型表和非事务型表。<br>非事务型表没有commit或者rollback的概念。</p><pre><code class="sql">SET AUTOCOMMIT=OFF;</code></pre><p>或者</p><pre><code class="sql">SET AUTOCOMMIT=0;</code></pre><p>能设置当前连接是否是自动提交的。不过对非事务型表没有作用。</p><ol><li><p>ACID属性</p><ul><li><strong>原子性</strong>:事务是不可分割的最小工作单元,整个事务要么全部提交要么全部回滚失败。</li><li><strong>一致性</strong>:数据库总是从一个一致性状态转换到另一个一致性的状态。</li><li><strong>隔离性</strong>: 一个事务所做的更改在最终提交之前其它事务是不可见的。</li><li><strong>持久性</strong>:事务一旦提交所做的修改就会永久保存在数据库中,即使系统崩溃,数据也不会丢失。</li></ul></li><li><p>隔离级别</p><ul><li><strong>未提交读(READ UNCOMMITTED)</strong>:未提交读隔离级别也叫读脏,就是事务可以读取其它事务未提交的数据。</li><li><strong>提交读(READ COMMITTED)</strong>:在其它数据库系统比如SQL Server默认的隔离级别就是提交读,已提交读隔离级别就是在事务未提交之前所做的修改其它事务是不可见的。</li><li><strong>可重复读(REPEATABLE READ)</strong>:保证同一个事务中的多次相同的查询的结果是一致的,比如一个事务一开始查询了一条记录然后过了几秒钟又执行了相同的查询,保证两次查询的结果是相同的,可重复读也是mysql的默认隔离级别。</li><li><p><strong>可串行化(SERIALIZABLE)</strong>:可串行化就是保证读取的范围内没有新的数据插入,比如事务第一次查询得到某个范围的数据,第二次查询也同样得到了相同范围的数据,中间没有新的数据插入到该范围中。<br> 查询和设置隔离级别:</p><pre><code class="sql"> # 查询系统默认隔离级别,当前会话隔离级别
select @@global.tx_isolation,@@tx_isolation;
# 设置系统隔离级别:
SET global transaction isolation level read committed;
# 设置会话隔离级别:
SET SESSION transaction isolation LEVEL read committed;</code></pre></li></ul></li></ol><h2>Replication概念</h2><p>复制意味着一份数据可以有多个副本,一个数据库中的数据,可以复制至另一个数据库。</p><p>复制在我们的生活中无处不在,同一份数据,有可能在你个人的电脑上有一份,U盘上有一份,云端的网盘上也可能会有一份。甚至在个人电脑里,同样一份数据也会有多个副本。<br>这里,我们之所以会将数据复制出多份,目的是显而易见的:<strong>备份</strong>。<br>当你抱着自己的电脑准备接上投影仪,准备向老板展示你苦战数个通宵后的PPT时,硬盘突然坏了,或者文件误删,莫名其妙的找不到了,没关系,你的U盘还有一份。什么!U盘忘了带了?没关系,你的网盘里还有一个。<br><strong>复制</strong>与<strong>备份</strong>的是两件不同的事情,可以通过复制来实现备份的目的。但是,复制的却不只是能提供备份而已。</p><h3>复制解决了什么问题</h3><p>在MySQL中,复制可以解决几个问题:</p><ul><li>数据分布</li><li>读写分离</li><li>备份</li><li>高可用和故障切换</li></ul><p>如果你们公司的数据中心位于全国各地,通过复制,可以实现异地备份,但异地的数据同步会有较大的时间延迟。<br>同时,如果主数据库的数据都同步复制至从库,那么当需要更新数据时,只需要更新主库即可,新更新的数据将会通过主库同步至从库,在这个基础上,便可以实现<strong>读写分离</strong>,即DML语句在主库上执行,而查询类SQL语句则在从库上执行,由于主从的同步有时延,因此这里的数据一致性模型并不满足强一致性,是最终一致性模型。<br>因为有了主从副本,所以当主库不可用(宕机,崩溃等原因),从库可以临危受命,升级为主库,保证数据库服务的高可用性。</p><h3>MySQL间如何复制</h3><p>分为3个步骤</p><ol><li>在主库上把数据更改记录到二进制(binary log)日志中。</li><li>从库把主库上的日志复制到自己的中继日志(relay log)中。</li><li>从库读取中继日志中的事件,将其重放在从库数据中。</li></ol><p><img src="/img/remote/1460000041528234" alt="image-20220310175315702" title="image-20220310175315702"></p><p>主库在每次事务准备提交前,按照事务的提交顺序,将更新事件记录到binary log中,并通知存储引擎提交事务。然后,从库会主动启动一个线程与主库建立连接,与此对应,主库会启动一个二进制转储(binlog dump)线程与之合作,将binary log发送给从库,从库接收并产生relay log,然后由从库的一个SQL线程负责将relay log还原为数据。</p><p>需要注意的是,从库在relay时,是单线程执行,换言之,串行执行的。</p><h2>Replication配置</h2><h3>复制的配置步骤</h3><ol><li>创建复制账号</li><li>配置主库和从库</li><li><p>通知从库开始进行复制</p><h3>基于二进制日志的主从复制</h3><h4>配置master</h4><p>通过在my.cnf中添加一个配置项<strong>log-bin</strong>,来起用master的二进制日志记录功能,如果没有配置log-bin,那么master的二进制记录功能并不会起用,log-bin在这里有两个作用</p></li><li>起用master的日志记录功能</li><li>指定日志文件名的前缀</li></ol><p>另外,需要在my.cnf中添加一个server-id配置项,用来指定master的唯一ID,范围可以是1到2^32-1。</p><pre><code>[mysqld]
# Replication Configurations(by beanlam)
server-id=1
log-bin=bin-log</code></pre><p>配置完成后需要重启mysql server。</p><h4>配置slave</h4><p>同样,slave也需要配置一个唯一的server-id,不允许跟master的server-id冲突,如果有多个slave,各个slave与master的server-id都应该是不同的。<br>slave也可以配置log-bin,配置了以后slave也可以和master一样,记录binary log。slave记录binary log有其用武之地的,比如数据备份和崩溃恢复,若当前的replication环境拓扑结构比较复杂,slave需要作为其它mysql server的master时,那么这个slave也必须启用binary log的功能。</p><pre><code>[mysqld]
# Replication Configurations(by beanlam)
server-id=2</code></pre><h4>创建一个账号用于复制</h4><p>slave获取master的binary log时,通过用户名和密码与master建立连接,可以创建一个专用于复制的账号,并只赋予这个账号与复制有关的权限。</p><pre><code class="sql">mysql> CREATE USER 'repl'@'%.mydomain.com' IDENTIFIED BY 'slavepass';
mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%.mydomain.com';</code></pre><p>除了REPLICATION SLAVE权限,还可以给用户授权REPLICATION CLIENT权限,这样用户就可以用来监控和管理复制。</p><h4>获取master二进制文件的坐标</h4><p>slave必须事先了解从master的二进制文件的哪个位置开始复制,因此需要先记录master当前二进制日志的坐标,坐标由文件名和偏移量决定。<br>获得master的二进制日志坐标,需要先保证没有写操作在进行。在master上执行以下语句:</p><pre><code class="sql">mysql> FLUSH TABLES WITH READ LOCK;</code></pre><p>这个语句能为表获得读锁,阻止其它写入操作。需要注意,执行这个语句的会话如果关闭,那么这个锁将会被释放,如果会话没有关闭,那么锁会一直持有。</p><p>在另一个会话里,用以下语句来查看master的二进制日志坐标</p><pre><code class="sql">mysql> show master status;
+----------------+----------+-------------------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------+----------+-------------------------+------------------+-------------------+
| bin-log.000005 | 1264 | beanlam_db1,beanlam_db2 | | |
+----------------+----------+-------------------------+------------------+-------------------+
1 row in set (0.00 sec)</code></pre><p>File和Position即表明了slave应该从何处开始进行复制。</p><p>如果master之前没有启用过二进制日志的功能,那么<code>show master status</code>查询结果将为空。这时对于slave来说,File是一个空字符串,Position是4,之所以是4,与二进制日志的文件格式有关。</p><h4>slave开始复制</h4><p>根据以上步骤,得到了master二进制文件的坐标后,只需要告诉slave,slave便可以埋头进入复制的状态中。</p><p>这里根据master是否有旧数据需要同步,分为两种情况:</p><ol><li><p>如果master是一个新的数据库服务器,其上没有任何旧的数据需要复制,那么就可以使用<code>change master to</code>命令为slave配置master的信息。</p><pre><code class="sql">CHANGE MASTER TO
MASTER_HOST='master_host_name',
MASTER_USER='replication_user_name',
MASTER_PASSWORD='replication_password',
MASTER_LOG_FILE='recorded_log_file_name',
MASTER_LOG_POS=recorded_log_position;</code></pre><p>配置完后,可以通过<code>start slave</code>命令正式开始进行复制。</p></li><li><p>如果master在启用二进制日志功能之前,已经存在有一部分数据,这一部分数据需要在复制开始之前,先同步至slave。<br>可以通过mysqldump工具对当前master的数据库数据做一个快照,生成一个dump文件,在slave开始复制之前,把这个文件的数据导入slave中。<br>基本的使用方法:</p><pre><code class="bash">mysqldump --all-databases --master-data > dbdump.db</code></pre><p><code>-all-databases</code>表明为所有数据库作快照,也可以用<code>--databases</code>来制定需要做快照的数据库。<br><code>--ignore-table</code>可以跳过数据库中的所有表<br><code>--master-data</code>会自动地在dump文件里加上<code>change master to</code>语句,启动slave复制,如果不加这个选项,则需要先开启一个新的会话,对所有的表加读。<br>当存储引擎是InnoDB时,推荐使用mysqldump。<br>另外一种方法是直接拷贝数据文件到slave。</p></li></ol><h2>Replication原理</h2><h3>基于语句的复制</h3><p>基于语句的复制也称为(逻辑复制),slave把master上造成数据更改的SQL语句在自己的库上也执行一次。</p><ul><li>优点<br>master的binlog只记录SQL语句,使得日志文件体积更小。</li><li>缺点</li></ul><p>master除了传输SQL语句给slave,还需要传输一些元数据,比如当前时间戳。<br>还有一些语句无法被正确复制,比如包含用户自定义函数的语句,这些函数可能有不确定的行为。以下函数可能导致非正常的复制:</p><pre><code>LOAD_FILE(), UUID(), UUID_SHORT(), USER(), FOUND_ROWS(), SYSDATE(), GET_LOCK(), IS_FREE_LOCK(), IS_USED_LOCK, MASTER_POS_WAIT, RAND(), RELEASE_LOCK(), SLEEP(), VERSION()</code></pre><p>此外,<code>INSERT......SELECT</code>语句需要获取更多的行级锁,比起基于行的复制来说。<br><code>UPDATE</code>语句可能导致全表扫描(where字句中没有包含索引字段)<br>如果使用的是InnoDB引擎,带有<code>auto_increment</code>的<code>insert</code>语句会堵塞其它非冲突的<code>insert</code>语句。<br>slave上的更新是串行的,因此需要更多的锁。另外,并不是所有的存储引擎都支持基于语句的复制。</p><h3>基于行的复制</h3><p>MySQL5.1开始支持基于行的复制</p><ul><li>优点<br>更少的锁</li><li>缺点<br>对于某些语句,例如插入或者删除语句,基于行的复制方式会将整行的数据都写进binary log,导致binary log体积很大,也导致需要持有锁的时间变长。<br>如果包含用户自定义函数,这些函数输出值非常大的文本,那么采取行的复制,会把这么大的文本也写进日志里。<br>在slave端看不到执行了哪些SQL语句<br>当使用MyISAM引擎时,<code>insert</code>语句需要获得重量级的锁,这意味着插入操作只能是串行的。</li></ul><h3>slave也作为master</h3><p>如果slave配置了log_slave_updates选项,slave也会像master一样记录binary log,从而可以作为一个master存在。</p><p><img src="/img/remote/1460000041528235" alt="image-20220310175430233" title="image-20220310175430233"></p><h3>拓扑结构</h3><h4>最常见的</h4><p><img src="/img/remote/1460000041528236" alt="image-20220310175524622" title="image-20220310175524622"></p><h4>不支持多主库复制</h4><p><img src="/img/remote/1460000041528237" alt="image-20220310175547780" title="image-20220310175547780"></p><h4>主动-被动模式下的主-主复制</h4><p>被动服务器时<strong>只读的</strong>(日志里记录的事件都带有一个server id,发现server id与自己的相同,则忽略这个事件)</p><p><img src="/img/remote/1460000041528238" alt="image-20220310175643089" title="image-20220310175643089"></p><h4>拥有备库的主-主结构</h4><p><img src="/img/remote/1460000041528239" alt="image-20220310175702861" title="image-20220310175702861"></p><h4>环形结构</h4><p>非常脆弱,其中一个节点失效会导致由这个节点发起的事件在其它节点之间链式死循环,因为只有它自己能过滤掉与自己server id相同的事件。</p><p><img src="/img/remote/1460000041528240" alt="image-20220310175732798" title="image-20220310175732798"></p><p>改进:</p><p><img src="/img/remote/1460000041528241" alt="image-20220310175747714" title="image-20220310175747714"></p><h4>主库-分发库-备库</h4><p>主要用在当多个备库执行复制请求时,导致主库负载过高时,可以引进分发库来减少主库的负载。</p><p><img src="/img/remote/1460000041528242" alt="image-20220310175819465" title="image-20220310175819465"></p><h4>树形或金字塔形</h4><p>故障处理过程更加复杂</p><p><img src="/img/remote/1460000041528243" alt="image-20220310175855537" title="image-20220310175855537"></p><h2>Replication 控制</h2><h3>控制Master</h3><h4>SHOW相关</h4><pre><code class="sql">show binary logs;/*相当于*/show master logs;
show binlog events;
show master status;
show slave hosts;</code></pre><p><img src="/img/remote/1460000041528244" alt="image-20220310180013888" title="image-20220310180013888"></p><p><img src="/img/remote/1460000041528245" alt="image-20220310180025822" title="image-20220310180025822"></p><p><img src="/img/remote/1460000041528246" alt="image-20220310180044097" title="image-20220310180044097"></p><h4>binary logs清除</h4><p>语法:</p><pre><code class="sql">PURGE { BINARY | MASTER } LOGS { TO 'log_name' | BEFORE datetime_expr }</code></pre><p>log_name或者datetime_expr之前的日志文件将会被删除,此处binary和master是同义词。<br>datetime_expr日期格式必须为'YYYY-MM-DD hh:mm:ss'。<br>例子:</p><pre><code class="sql">PURGE BINARY LOGS TO 'mysql-bin.010';
PURGE BINARY LOGS BEFORE '2008-04-02 22:46:26';</code></pre><p>当slave正在从master上复制时,上述的语句的执行也是安全的,如果要删除的文件正在被slave读取,那么这个文件将不会被删除。<br>如果删除了slave还未复制的日志文件,那么slave无法同步这部分被删除的数据。</p><p>当要清楚binary logs时,以下步骤是最佳实践:</p><ul><li>在每个slave上用<code>show slave status</code>查看正在同步哪个binary log。</li><li>在master上执行<code>show binary logs</code>查看日志文件列表</li><li>在列表中,找出所有slave正在同步的binary log中那个最靠前的,准备删除这个log之前的logs。</li><li>为即将被删除的Logs作备份(非必须,但建议这么做)</li><li>最后用<code>purge</code>命令清除日志。</li></ul><p>purge命令是根据.index文件里所列的日志文件来进行删除的(<em>.index的前缀就是binlog的名字,这个文件是由mysqld维护的</em>),如果一个日志文件在操作系统中不存在(例如被人为地通过<code>rm</code>命令删除),而.index文件里又记录了这个日志文件,那么purge命令会报错。</p><h4>重置Master</h4><p>语法:</p><pre><code class="sql">reset master;</code></pre><p>这个命令会删除.index文件里所列举的所有binary logs,清空.index文件并产生一个新的空的binary log文件。</p><p>这个命令还会重置<code>gtid_purged</code>和<code> gtid_executed</code>这两个系统变量为空字符串(会话范围内的变量值不会被重置),并且从MySQL5.7.5开始,这个命令还会清空<code>mysql.gtid_executed</code>表。</p><blockquote>reset命令和purge命令的区别<br>当有slave正在进行数据同步时,不应该使用(也不支持)reset master命令,可能带来未知的后果,不过purge命令则可以在slave运行时安全地使用。</blockquote><p>当在测试环境中,需要经常初始化设置master和slave时,使用<code>reset master</code>的好处多多,可以按以下步骤初始master-slave:</p><ul><li>分别启动master和slave,并启动replication</li><li>在master上执行一些语句</li><li>检查master的更新是否同步到slave</li><li>如果slave正确同步了master的数据,在slave上执行<code>stop slave</code>命令,然后执行<code>reset slave</code>,并确认数据是否删除了数据。</li><li>在master上执行<code>reset master</code>命令</li></ul><p>通过以上步骤,在测试的时候,就可以清空master的binary log,并且重新开始replication的测试。</p><h4>暂停replication</h4><p>命令:</p><pre><code class="sql">set sql_log_bin=0|1;</code></pre><p>0, 不同步至slave<br>1, 同步至slave</p><p>可以在session内进行设置,也可以在全局范围内设置(设置后新建的session会有影响,但已经存在的session不会受影响)</p><h3>控制Slave</h3><h4>SHOW相关</h4><pre><code class="sql">show slave status;
show relaylog events;</code></pre><h4>指向Master</h4><p>语法:</p><pre><code class="sql">CHANGE MASTER TO option [, option] ... [ channel_option ]
option:
MASTER_BIND = 'interface_name'
| MASTER_HOST = 'host_name'
| MASTER_USER = 'user_name'
| MASTER_PASSWORD = 'password'
| MASTER_PORT = port_num
| MASTER_CONNECT_RETRY = interval
| MASTER_RETRY_COUNT = count
| MASTER_DELAY = interval
| MASTER_HEARTBEAT_PERIOD = interval
| MASTER_LOG_FILE = 'master_log_name'
| MASTER_LOG_POS = master_log_pos
| MASTER_AUTO_POSITION = {0|1}
| RELAY_LOG_FILE = 'relay_log_name'
| RELAY_LOG_POS = relay_log_pos
| MASTER_SSL = {0|1}
| MASTER_SSL_CA = 'ca_file_name'
| MASTER_SSL_CAPATH = 'ca_directory_name'
| MASTER_SSL_CERT = 'cert_file_name'
| MASTER_SSL_CRL = 'crl_file_name'
| MASTER_SSL_CRLPATH = 'crl_directory_name'
| MASTER_SSL_KEY = 'key_file_name'
| MASTER_SSL_CIPHER = 'cipher_list'
| MASTER_SSL_VERIFY_SERVER_CERT = {0|1}
| MASTER_TLS_VERSION = 'protocol_list'
| IGNORE_SERVER_IDS = (server_id_list)
channel_option:
FOR CHANNEL channel
server_id_list:
[server_id [, server_id] ... ]</code></pre><p>CHANGE MASTER TO用来重新设置slave,使其指向新的master,或者仅仅是改变一些上面提到的option。这个命令在MySQL5.7.4及其以上版本增加了许多特性,例如channel的概念等等。待补充。</p><h4>replication过滤</h4><p>CHANGE REPLICATION FILTER 语法</p><pre><code class="sql">CHANGE REPLICATION FILTER filter[, filter][, ...]
filter:
REPLICATE_DO_DB = (db_list)
| REPLICATE_IGNORE_DB = (db_list)
| REPLICATE_DO_TABLE = (tbl_list)
| REPLICATE_IGNORE_TABLE = (tbl_list)
| REPLICATE_WILD_DO_TABLE = (wild_tbl_list)
| REPLICATE_WILD_IGNORE_TABLE = (wild_tbl_list)
| REPLICATE_REWRITE_DB = (db_pair_list)
db_list:
db_name[, db_name][, ...]
tbl_list:
db_name.table_name[, db_table_name][, ...]
wild_tbl_list:
'db_pattern.table_pattern'[, 'db_pattern.table_pattern'][, ...]
db_pair_list:
(db_pair)[, (db_pair)][, ...]
db_pair:
from_db, to_db</code></pre><p>从MySQL5.7.3开始,<code>CHANGE REPLICATION FILTER</code>命令用来为slave设置一个或多个复制过滤规则,比如说`--<br>replicate-do-db<code>或者 </code>--replicate-wild-ignore-table<code>,这些选项不像服务器选项,重置后还需要重启mysql才生效,这些选项可以动态修改,只需要先停止slave的SQL线程,设置后,再重启SQL线程(</code>start|stop slave sql_thread`)。</p><p>各个选项的作用,待补充。</p><h4>MASTER_POS_WAIT()</h4><p>确切来说,这个一个函数,而非SQL语句 。</p><pre><code class="sql">SELECT MASTER_POS_WAIT('master_log_file', master_log_pos [, timeout][, channel])</code></pre><p>这里的file和pos对应主库show master status得到的值,代表执行位置。 函数逻辑是等待当前从库达到这个位置后返回, 返回期间执行的事务个数。<br>参数timeout可选,若缺省则无限等待,timeout<=0时与缺省的逻辑相同。若为正数,则等待这么多秒,超时函数返回-1.<br>其他返回值:若当前slave为启动或在等待期间被终止,返回NULL; 若指定的值已经在之前达到,返回0。</p><h4>重置slave</h4><p>语法</p><pre><code class="sql">RESET SLAVE [ALL] [channel_option]
channel_option:
FOR CHANNEL channel</code></pre><p>这个命令清除了slave保存的关于master和relay log的信息,删除所有的relay log。使用这个命令,slave的replication线程必须先停下来。</p><p>这个命令还有影响到channel,待补充。</p><p><code>reset slave</code>不会改变slave与master的连接信息,比如master的ip地址,端口,用户名和密码等,<code>reset slave all</code>将会重置连接的信息,因此这意味着需要重启slave的mysqld进程。</p><h4>跳过执行</h4><pre><code class="sql">SET GLOBAL sql_slave_skip_counter = N</code></pre><h4>start slave</h4><p>语法:</p><pre><code class="sql">START SLAVE [thread_types] [until_option] [connection_options] [channel_option]
thread_types:
[thread_type [, thread_type] ... ]
thread_type:
IO_THREAD | SQL_THREAD
until_option:
UNTIL { {SQL_BEFORE_GTIDS | SQL_AFTER_GTIDS} = gtid_set
| MASTER_LOG_FILE = 'log_name', MASTER_LOG_POS = log_pos
| RELAY_LOG_FILE = 'log_name', RELAY_LOG_POS = log_pos
| SQL_AFTER_MTS_GAPS }
connection_options:
[USER='user_name'] [PASSWORD='user_pass'] [DEFAULT_AUTH='plugin_name'] [PLUGIN_DIR='plugin_dir']
channel_option:
FOR CHANNEL channel
gtid_set:
uuid_set [, uuid_set] ...
| ''
uuid_set:
uuid:interval[:interval]...
uuid:
hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh
h:
[0-9,A-F]
interval:
n[-n]
(n >= 1)</code></pre><p>不指定thread_type时,IO线程和SQL线程都会被启动,IO线程从master读取日志,SQL线程读取relay log并执行。</p><h4>stop slave</h4><p>语法</p><pre><code class="sql">STOP SLAVE [thread_types]
thread_types:
[thread_type [, thread_type] ... ]
thread_type: IO_THREAD | SQL_THREAD
channel_option:
FOR CHANNEL channel</code></pre><p>当停止slave服务器时,应先执行<code>stop slave</code>停止slave功能。</p><p>执行这个命令时,还需要考虑是基于行的replication还是基于语句的replication,存储引擎,以及事务型表和非事务型表。</p><h3>控制Group复制</h3><pre><code class="sql">start group_replication;
stop group_replication;</code></pre><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Java 实现配置加载机制
https://segmentfault.com/a/1190000004347872
2016-01-21T23:44:16+08:00
2016-01-21T23:44:16+08:00
ytbean
https://segmentfault.com/u/ytbean
2
<p>原文链接:<a href="https://link.segmentfault.com/?enc=qSCha%2BLoQRsuVFE%2BjxxPOA%3D%3D.BMA%2FRIyCvHylMlqEGVtwhS9098mPhWLp9DuM7IudLTbgmqlutN%2Fppbhjgz8jbxkQzaqNAHX4AFqOAsNMGvyYcg%3D%3D" rel="nofollow">《Java 实现配置加载机制》http://www.ytbean.com/posts/java-config-skeleton/</a></p><h2>前言</h2><p>现如今几乎大多数Java应用,例如我们耳熟能详的tomcat, struts2, netty...等等数都数不过来的软件,<br>要满足通用性,都会提供配置文件供使用者定制功能。</p><p>甚至有一些例如Netty这样的网络框架,几乎完全就是由配置驱动,这样的软件我们也通常称之为"微内核架构"的软件。<br>你把它配置成什么,它就是什么。</p><blockquote>It is what you configure it to be.</blockquote><p>最常见的配置文件格式是XML, Properties等等文件。</p><p>本文探讨加载配置中最通用也是最常见的场景,那就是把一个配置文件映射成Java里的POJO对象.<br>并探讨如何实现不同方式的加载,例如,有一些配置是从本地XML文件里面加载的,而有一些配置需要从本地Properties文件加载,<br>更有甚者,有一些配置需要通过网络加载配置。</p><p>如何实现这样一个配置加载机制,让我们拥有这个机制后,不会让加载配置的代码散布得到处都是,并且可扩展,可管理。</p><h2>配置加载器</h2><p>首先,我们需要一个配置加载器,而这个配置加载器是可以有多种不同的加载方式的,因此,我们用一个接口来描述它,如下所示:</p><pre><code class="java">/**
*
*
* @author Bean
* @date 2016年1月21日 上午11:47:12
* @version 1.0
*
*/
public interface IConfigLoader<T> {
/**
* load the config typed by T
*
* @return
* @throws ConfigException
*/
public T load() throws ConfigException;
}</code></pre><p>可是,为什么我们需要在这个接口上声明泛型<code><T></code> ?<br>很明显,当我们要使用一个配置加载器时,你得告诉这个配置加载器你需要加载后得到什么结果。<br>例如,你希望加载配置后得到一个<code>AppleConfig</code>对象,那么你就可以这么去使用上述定义的接口:</p><pre><code class="java"> IConfigLoader<AppleConfig> loader = new AppleConfigLoader<AppleConfig>();
AppleConfig config = loader.load();</code></pre><p>于是你将配置文件里的信息转化成了一个AppleConfig对象,并且你能得到这个AppleConfig对象实例。</p><p>到目前,貌似只要我们的<code>AppleConfigLoader</code>里面实现了怎么加载配置文件的具体劳动,我们就可以轻易加载配置了。</p><p>可以这么说,但是不是还没有考虑到,配置可能通过不同的方式加载呢,比如通过Properties加载,通过dom方式加载,通过sax方式加载,或者通过某些第三方的开源库来加载。</p><p>因此,除了<code>配置加载器</code>,我们还需要另外一种角色,配置加载方式的提供者。暂且,我们就叫它IConfigProvider。</p><h2>配置加载方式的提供者</h2><p>配置加载方式的提供者可以提供一种加载方式给配置加载器,换言之,提供一个<code>对象</code>给配置加载器。</p><ul><li>如果通过dom方式加载,那么<strong>提供者</strong>提供一个<code>Document</code>对象给<strong>加载器</strong>。</li><li>如果通过Properties方式加载,那么<strong>提供者</strong>提供一个<code>Properties</code>对象给<strong>加载器</strong></li><li>如果通过第三方类库提供的方式加载,比如apache-commons-digester3(tomcat的配置加载),那么<strong>提供者</strong>提供一个<code>Digester</code>对象给<strong>加载器</strong></li></ul><p><strong>提供者</strong>的职责就是<strong>提供</strong>,仅此而已,只提供配置加载器所需要的对象,但它本身并不参与配置加载的劳动。</p><p>我们用一个接口<code>IConfigProvider</code>来定义这个<strong>提供者</strong></p><pre><code class="java">/**
*
*
* @author Bean
* @date 2016年1月21日 上午11:54:28
* @version 1.0
*
*/
public interface IConfigProvider<T> {
/**
* provide a config source used for loading config
*
* @return
* @throws ConfigException
*/
public T provide() throws ConfigException;
}
</code></pre><p>这里为什么又会有<code><T></code>来声明泛型呢?<br>如果需要一个提供者,那么至少得告诉这个提供者它该提供什么吧。</p><p>因此,一个提供者会提供什么,由这个<T>来决定。</p><p>同时,到这里,我们可以先建造一个工厂,让它来生产特定的提供者:</p><pre><code class="java">/**
*
*
* @author Bean
* @date 2016年1月21日 上午11:56:28
* @version 1.0
*
*/
public class ConfigProviderFactory {
private ConfigProviderFactory() {
throw new UnsupportedOperationException("Unable to initialize a factory class : "
+ getClass().getSimpleName());
}
public static IConfigProvider<Document> createDocumentProvider(String filePath) {
return new DocumentProvider(filePath);
}
public static IConfigProvider<Properties> createPropertiesProvider(String filePath) {
return new PropertiesProvider(filePath);
}
public static IConfigProvider<Digester> createDigesterProvider(String filePath) {
return new DigesterProvider(filePath);
}
}</code></pre><h2>可以开始实现具体配置加载器了?</h2><p>还不行!</p><p>到这里,假设我们有一个配置文件,叫apple.xml。而且我们要通过DOM方式把这一份apple.xml加载后变成AppleConfig对象。</p><p>那么,首先我要通过提供者工厂给我制造一个能提供Document的提供者。然后拿到这个提供者,我就可以调用它的provide方法来获得Document对象,<br>有了document对象,那么我就可以开始来加载配置了。</p><p>可是,如果要加载BananaConfig、PearConfig.......呢,其步骤都是一样的。因此我们还要有一个抽象类,来实现一些默认的共同行为。</p><pre><code class="java">/**
*
*
* @author Bean
* @date 2016年1月21日 上午11:59:19
* @version 1.0
*
*/
public abstract class AbstractConfigLoader <T, U> implements IConfigLoader<T>{
protected IConfigProvider<U> provider;
protected AbstractConfigLoader(IConfigProvider<U> provider) {
this.provider = provider;
}
/*
* @see IConfigLoader#load()
*/
@Override
public T load() throws ConfigException {
return load(getProvider().provide());
}
public abstract T load(U loaderSource) throws ConfigException;
protected IConfigProvider<U> getProvider() {
return this.provider;
}
}</code></pre><p>每个配置加载器都有一个带参数构造器,接收一个Provider。</p><p>泛型<T>指明了我要加载的是AppleConfig还是BananConfig,泛型<code><U></code>指明了要用什么加载方式加载,是Document呢,还是Properties,或者其他。</p><h2>实战运用实例</h2><p>有一份菜市场配置文件market.xml,配置了菜市场的商品,里面有两种商品,分别是苹果和鸡蛋。</p><pre><code class="xml"><market>
<apple>
<color>red</color>
<price>100</price>
</apple>
<egg>
<weight>200</weight>
</egg>
</market></code></pre><p>另外还有一份关于各个档口老板名字的配置文件,owner.properties</p><pre><code>port1=Steve Jobs
port2=Bill Gates
port3=Kobe Bryant</code></pre><p>我们先定义好如下类:<br>MarketConfig.java</p><pre><code class="java">/**
*
*
* @author Bean
* @date 2016年1月21日 下午11:03:37
* @version 1.0
*
*/
public class MarketConfig {
private AppleConfig appleConfig;
private EggConfig eggConfig;
private OwnerConfig ownerConfig;
public AppleConfig getAppleConfig() {
return appleConfig;
}
public void setAppleConfig(AppleConfig appleConfig) {
this.appleConfig = appleConfig;
}
public EggConfig getEggConfig() {
return eggConfig;
}
public void setEggConfig(EggConfig eggConfig) {
this.eggConfig = eggConfig;
}
public OwnerConfig getOwnerConfig() {
return ownerConfig;
}
public void setOwnerConfig(OwnerConfig ownerConfig) {
this.ownerConfig = ownerConfig;
}
}</code></pre><p>AppleConfig.java</p><pre><code class="java">
/**
*
*
* @author Bean
* @date 2016年1月21日 下午11:03:45
* @version 1.0
*
*/
public class AppleConfig {
private int price;
private String color;
public void setPrice(int price) {
this.price = price;
}
public int getPrice() {
return this.price;
}
public void setColor(String color) {
this.color = color;
}
public String getColor() {
return this.color;
}
}</code></pre><p>EggConfig.java</p><pre><code class="java">
/**
*
*
* @author Bean
* @date 2016年1月21日 下午11:03:58
* @version 1.0
*
*/
public class EggConfig {
private int weight;
public void setWeight(int weight) {
this.weight = weight;
}
public int getWeight() {
return this.weight;
}
}
</code></pre><p>OwnerConfig.java</p><pre><code class="java">
/**
*
*
* @author Bean
* @date 2016年1月21日 下午11:04:06
* @version 1.0
*
*/
public class OwnerConfig {
private Map<String, String> owner = new HashMap<String, String>();
public void addOwner(String portName, String owner) {
this.owner.put(portName, owner);
}
public String getOwnerByPortName(String portName) {
return this.owner.get(portName);
}
public Map<String, String> getOwners() {
return Collections.unmodifiableMap(this.owner);
}
}</code></pre><p>这个例子有两种配置加载方式,分别是Dom和Properties加载方式。<br>所以我们的提供者建造工厂需要制造两种提供者provider.<br>而且需要定义2个配置加载器,分别是:</p><p>OwnerConfigLoader</p><pre><code class="java">/**
*
*
* @author Bean
* @date 2016年1月21日 下午11:24:50
* @version 1.0
*
*/
public class OwnerConfigLoader extends AbstractConfigLoader<OwnerConfig, Properties>{
/**
* @param provider
*/
protected OwnerConfigLoader(IConfigProvider<Properties> provider) {
super(provider);
}
/*
* @see AbstractConfigLoader#load(java.lang.Object)
*/
@Override
public OwnerConfig load(Properties props) throws ConfigException {
OwnerConfig ownerConfig = new OwnerConfig();
/**
* 利用props,设置ownerConfig的属性值
*
* 此处代码省略
*/
return ownerConfig;
}
}
</code></pre><p>然后是MarketConfigLoader</p><pre><code class="java">import org.w3c.dom.Document;
/**
*
*
* @author Bean
* @date 2016年1月21日 下午11:18:56
* @version 1.0
*
*/
public class MarketConfigLoader extends AbstractConfigLoader<MarketConfig, Document> {
/**
* @param provider
*/
protected MarketConfigLoader(IConfigProvider<Document> provider) {
super(provider);
}
/*
* AbstractConfigLoader#load(java.lang.Object)
*/
@Override
public MarketConfig load(Document document) throws ConfigException {
MarketConfig marketConfig = new MarketConfig();
AppleConfig appleConfig = new AppleConfig();
EggConfig eggConfig = new EggConfig();
/**
* 在这里处理document,然后就能得到
* AppleConfig和EggConfg
*
* 此处代码省略
*/
marketConfig.setAppleConfig(appleConfig);
marketConfig.setEggConfig(eggConfig);
/**
* 由于OwnerConfig是需要properties方式来加载,不是xml
* 所以这里要新建一个OwnerConfigLoader,委托它来加载OwnerConfig
*/
OwnerConfigLoader ownerConfigLoader = new OwnerConfigLoader(ConfigProviderFactory.createPropertiesProvider(YOUR_FILE_PATH));
OwnerConfig ownerConfig = ownerConfigLoader.load();
marketConfig.setOwnerConfig(ownerConfig);
return marketConfig;
}
}</code></pre><p>然后,我们在应用层面如何获取到MarketConfig呢</p><pre><code>MarketConfigLoader marketConfigLoader = new MarketConfigLoader(ConfigProviderFactory.createDocumentProvider(YOUR_FILE_PATH));
MarketConfig marketConfig = marketConfigLoader.load();
</code></pre><p>也许有个地方会人奇怪,明明有四个配置类,为什么只有2个配置加载器呢。<br>因为MarketConfig、EggConfig和AppleConfig,都是从同一个xml配置文件里面加载,所以只要一个Document对象,通过MarketConfigLoader就可以全部加载。</p><p>而OwnerConfig是不同的加载方式,所以需要另外一个加载器。</p><h2>尾声</h2><p>本文提出的配置加载机制,并不能够实际帮忙加载配置,这事应该留给DOM,SAX,以及其他一些开源库如dom4j,Digester去做。<br>但本文提出的配置加载机制能够让配置加载机制更灵活,容易扩展,并且能够集成多种配置加载方式,融合到一个机制进来,发挥各自有点。</p><p>实际上,有些软件经常需要同时从多种不同格式的配置文件里面加载配置,例如struts2,以及我最近在研究并被气到吐血的某国产开源数据库中间件软件,<br>如果没有一套完整的配置加载机制,那么代码会比较散乱,可维护性不高。容易使人吐血。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Java 实现生命周期管理机制
https://segmentfault.com/a/1190000004296533
2016-01-13T00:04:58+08:00
2016-01-13T00:04:58+08:00
ytbean
https://segmentfault.com/u/ytbean
5
<p>原文链接:<a href="https://link.segmentfault.com/?enc=qsuIBNEWfotlSbpvxi23AA%3D%3D.7mMlPi4%2Fsbgr1ZvS2iqJLKx1dF9AyoOm1%2BBykVyKnZ6OGVhg4yDmJIycao9qT6gF" rel="nofollow">《Java 实现生命周期管理机制》http://www.ytbean.com/posts/java-lifecycle/</a></p><h2>先扯一下</h2><p>最近一直在研究某个国产开源的MySQL数据库中间件,拉下其最新版的代码到eclipse后,启动起来,然后做各种测试和代码追踪;用完想要关闭它时,拉出它的STOP类想要运行时,发现这个类里赫然只写以下几行代码,于是我感觉瞬间受到了很多伤害。</p><pre><code class="java"> public static void main(String[] args) {
System.out.println(new Date() + ",server shutdown!");
}</code></pre><p>这个中间件启动和运行的时候,开启了监听,启动着许多线程在跑着,并且有许多socket连接。但是并没有找到一个优雅的方式将其关闭。于是无奈之下,我只能去点eclipse的心碎小红点,强行停掉VM。</p><p>如果是一个架构良好,模块化清晰的软件,特别是Server类的软件,拥有一套生命周期管理机制是非常重要的。不仅可以管理各个模块的生命周期,也可以在启停整个软件的时候更优雅,不会漏掉任何资源。</p><h2>生命周期状态</h2><p>一个模块的生命周期状态一般有以下几个:</p><blockquote>新生 -> 初始化中 -> 初始化完成 -> 启动中 -> 启动完成 -> 正在暂停 -> 已经暂停 -> 正在恢复 -> 已经恢复 -> 正在销毁 -> 已经销毁</blockquote><p>其中,任何一个状态之间的转化如果失败,那么就会进入另外一种状态:失败。</p><p>为此,可以用一个枚举类来枚举出这几个状态,如下所示:</p><pre><code class="java">public enum LifecycleState {
NEW, //新生
INITIALIZING, INITIALIZED, //初始化
STARTING, STARTED, //启动
SUSPENDING, SUSPENDED, //暂停
RESUMING, RESUMED,//恢复
DESTROYING, DESTROYED,//销毁
FAILED;//失败
}</code></pre><h2>接口</h2><p>生命周期中的各种行为规范,也需要一个接口来定义,如下所示:</p><pre><code class="java">public interface ILifecycle {
/**
* 初始化
*
* @throws LifecycleException
*/
public void init() throws LifecycleException;
/**
* 启动
*
* @throws LifecycleException
*/
public void start() throws LifecycleException;
/**
* 暂停
*
* @throws LifecycleException
*/
public void suspend() throws LifecycleException;
/**
* 恢复
*
* @throws LifecycleException
*/
public void resume() throws LifecycleException;
/**
* 销毁
*
* @throws LifecycleException
*/
public void destroy() throws LifecycleException;
/**
* 添加生命周期监听器
*
* @param listener
*/
public void addLifecycleListener(ILifecycleListener listener);
/**
* 删除生命周期监听器
*
* @param listener
*/
public void removeLifecycleListener(ILifecycleListener listener);
}</code></pre><p>发生生命周期状态转化时,可能需要触发对某类事件感兴趣的监听者,因此<code>ILifeCycle</code>也定义了两个方法可以添加和移除监听者。分别是:<code>public void addLifecycleListener(ILifecycleListener listener);</code>和<code> public void removeLifecycleListener(ILifecycleListener listener);</code></p><p>监听者也由一个接口来定义其行为规范,如下所示:</p><pre><code class="java">public interface ILifecycleListener {
/**
* 对生命周期事件进行处理
*
* @param event 生命周期事件
*/
public void lifecycleEvent(LifecycleEvent event);
}</code></pre><p>生命周期事件由<code>LifecycleEvent</code>来表示,如下所示:</p><pre><code class="java">public final class LifecycleEvent {
private LifecycleState state;
public LifecycleEvent(LifecycleState state) {
this.state = state;
}
/**
* @return the state
*/
public LifecycleState getState() {
return state;
}
}</code></pre><h2>骨架实现</h2><p>有了ILifeCycle接口以后,任何实现了这个接口的类将会被作为一个生命周期管理对象,这个类可以是一个socket监听服务,也可以代表一个特定的模块,等等。那我们是不是只要实现ILifeCycle就可以了? 可以这么说,但考虑到各个生命周期管理对象在生命周期的各个阶段会有一些共同的行为,比如说:</p><ul><li>设置自身的生命周期状态</li><li>检查状态的转换是否符合逻辑</li><li>通知监听者生命周期状态发生了变化</li></ul><p>因此,提供一个抽象类<code>AbstractLifeCycle</code>,作为<code>ILifeCycle</code>的<strong>骨架实现</strong>是有重要意义的,这样避免了很多的重复代码,使得架构更加清晰。这个抽象类会实现<code>ILifeCycle</code>中定义的所有接口方法,并添加对应的抽象方法,供子类实现。<code>AbstractLifeCycle</code>可以这么实现:</p><pre><code class="java">public abstract class AbstractLifecycle implements ILifecycle {
private List<ILifecycleListener> listeners = new CopyOnWriteArrayList<ILifecycleListener>();
/**
* state 代表当前生命周期状态
*/
private LifecycleState state = LifecycleState.NEW;
/*
* @see ILifecycle#init()
*/
@Override
public final synchronized void init() throws LifecycleException {
if (state != LifecycleState.NEW) {
return;
}
setStateAndFireEvent(LifecycleState.INITIALIZING);
try {
init0();
} catch (Throwable t) {
setStateAndFireEvent(LifecycleState.FAILED);
if (t instanceof LifecycleException) {
throw (LifecycleException) t;
} else {
throw new LifecycleException(formatString(
"Failed to initialize {0}, Error Msg: {1}", toString(), t.getMessage()), t);
}
}
setStateAndFireEvent(LifecycleState.INITIALIZED);
}
protected abstract void init0() throws LifecycleException;
/*
* @see ILifecycle#start()
*/
@Override
public final synchronized void start() throws LifecycleException {
if (state == LifecycleState.NEW) {
init();
}
if (state != LifecycleState.INITIALIZED) {
return;
}
setStateAndFireEvent(LifecycleState.STARTING);
try {
start0();
} catch (Throwable t) {
setStateAndFireEvent(LifecycleState.FAILED);
if (t instanceof LifecycleException) {
throw (LifecycleException) t;
} else {
throw new LifecycleException(formatString("Failed to start {0}, Error Msg: {1}",
toString(), t.getMessage()), t);
}
}
setStateAndFireEvent(LifecycleState.STARTED);
}
protected abstract void start0() throws LifecycleException;
/*
* @see ILifecycle#suspend()
*/
@Override
public final synchronized void suspend() throws LifecycleException {
if (state == LifecycleState.SUSPENDING || state == LifecycleState.SUSPENDED) {
return;
}
if (state != LifecycleState.STARTED) {
return;
}
setStateAndFireEvent(LifecycleState.SUSPENDING);
try {
suspend0();
} catch (Throwable t) {
setStateAndFireEvent(LifecycleState.FAILED);
if (t instanceof LifecycleException) {
throw (LifecycleException) t;
} else {
throw new LifecycleException(formatString("Failed to suspend {0}, Error Msg: {1}",
toString(), t.getMessage()), t);
}
}
setStateAndFireEvent(LifecycleState.SUSPENDED);
}
protected abstract void suspend0() throws LifecycleException;
/*
* @see ILifecycle#resume()
*/
@Override
public final synchronized void resume() throws LifecycleException {
if (state != LifecycleState.SUSPENDED) {
return;
}
setStateAndFireEvent(LifecycleState.RESUMING);
try {
resume0();
} catch (Throwable t) {
setStateAndFireEvent(LifecycleState.FAILED);
if (t instanceof LifecycleException) {
throw (LifecycleException) t;
} else {
throw new LifecycleException(formatString("Failed to resume {0}, Error Msg: {1}",
toString(), t.getMessage()), t);
}
}
setStateAndFireEvent(LifecycleState.RESUMED);
}
protected abstract void resume0() throws LifecycleException;
/*
* @see ILifecycle#destroy()
*/
@Override
public final synchronized void destroy() throws LifecycleException {
if (state == LifecycleState.DESTROYING || state == LifecycleState.DESTROYED) {
return;
}
setStateAndFireEvent(LifecycleState.DESTROYING);
try {
destroy0();
} catch (Throwable t) {
setStateAndFireEvent(LifecycleState.FAILED);
if (t instanceof LifecycleException) {
throw (LifecycleException) t;
} else {
throw new LifecycleException(formatString("Failed to destroy {0}, Error Msg: {1}",
toString(), t.getMessage()), t);
}
}
setStateAndFireEvent(LifecycleState.DESTROYED);
}
protected abstract void destroy0() throws LifecycleException;
/*
* @see
* ILifecycle#addLifecycleListener(ILifecycleListener)
*/
@Override
public void addLifecycleListener(ILifecycleListener listener) {
listeners.add(listener);
}
/*
* @see
* ILifecycle#removeLifecycleListener(ILifecycleListener)
*/
@Override
public void removeLifecycleListener(ILifecycleListener listener) {
listeners.remove(listener);
}
private void fireLifecycleEvent(LifecycleEvent event) {
for (Iterator<ILifecycleListener> it = listeners.iterator(); it.hasNext();) {
ILifecycleListener listener = it.next();
listener.lifecycleEvent(event);
}
}
protected synchronized LifecycleState getState() {
return state;
}
private synchronized void setStateAndFireEvent(LifecycleState newState) throws LifecycleException {
state = newState;
fireLifecycleEvent(new LifecycleEvent(state));
}
private String formatString(String pattern, Object... arguments) {
return MessageFormat.format(pattern, arguments);
}
/*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return getClass().getSimpleName();
}
}
</code></pre><p>可以看到,抽象类的骨架实现中做了几件生命周期管理中通用的事情,检查状态之间的转换是否合法(比如说start之前必须要init),设置内部状态,以及触发相应的监听者。</p><p>抽象类实现了<code>ILifeCycle</code>定义的方法后,又留出了相应的抽象方法供其子类实现,如上面的代码所示,其留出来的抽象方法有以下这些:</p><pre><code class="java">protected abstract void init0() throws LifecycleException;
protected abstract void start0() throws LifecycleException;
protected abstract void suspend0() throws LifecycleException;
protected abstract void resume0() throws LifecycleException;
protected abstract void destroy0() throws LifecycleException;</code></pre><h2>优雅的实现</h2><p>到目前为止,我们已经定义了接口<code>ILifeCycle</code>,以及其骨架实现<code>AbstractLifeCycle</code>,并且增加了监听者机制。貌似我们可以开始写一个类来继承<code>AbstractLifecycle</code>,并重写其定义的抽象方法了,so far so good。</p><p>但在开始之前,我们还需要考虑另外几个问题,</p><ul><li>我们的实现类是否对所有的抽象方法都感兴趣?</li><li>是否每个实现累都需要实现<code>init0</code>, <code>start0</code>, <code>suspend0</code>, <code>resume0</code>, <code>destroy0</code>?</li><li>是否有时候,我们的那些有生命的类或者模块并不支持暂停(suspend),恢复(resume)?</li></ul><p>直接继承<code>AbstractLifeCycle</code>,就意味着必须实现其全部的抽象方法。<br>因此,我们还需要一个默认实现,<code>DefaultLifeCycle</code>,让它继承<code>AbstractLifeCycle</code>,并实现所有抽象方法,但它并不做任何实际的事情, do nothing。只是让我们真正的实现类来继承这个默认的实现类,并重写感兴趣的方法。</p><p>于是,我们的<code>DefaultLifeCycle</code>就这么诞生了:</p><pre><code class="java">public class DefaultLifecycle extends AbstractLifecycle {
/*
* @see AbstractLifecycle#init0()
*/
@Override
protected void init0() throws LifecycleException {
// do nothing
}
/*
* @see AbstractLifecycle#start0()
*/
@Override
protected void start0() throws LifecycleException {
// do nothing
}
/*
* @see AbstractLifecycle#suspend0()
*/
@Override
protected void suspend0() throws LifecycleException {
// do nothing
}
/*
* @see AbstractLifecycle#resume0()
*/
@Override
protected void resume0() throws LifecycleException {
// do nothing
}
/*
* @see AbstractLifecycle#destroy0()
*/
@Override
protected void destroy0() throws LifecycleException {
// do nothing
}
}</code></pre><p>对于<code>DefaultLifeCycle</code>来说,do nothing就是其职责。<br>因此接下来我们可以写一个自己的实现类,继承<code>DefaultLifeCycle</code>,并重写那些感兴趣的生命周期方法。</p><p>例如,我有一个类只需要在初始化,启动,和销毁时做一些任务,那么可以这么写:</p><pre><code class="java">
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServer extends DefaultLifecycle {
private ServerSocket acceptor = null;
private int port = 9527;
/*
* @see DefaultLifecycle#init0()
*/
@Override
protected void init0() throws LifecycleException {
try {
acceptor = new ServerSocket(port);
} catch (IOException e) {
throw new LifecycleException(e);
}
}
/*
* @see DefaultLifecycle#start0()
*/
@Override
protected void start0() throws LifecycleException {
Socket socket = null;
try {
socket = acceptor.accept();
//do something with socket
} catch (IOException e) {
throw new LifecycleException(e);
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
/*
* @see DefaultLifecycle#destroy0()
*/
@Override
protected void destroy0() throws LifecycleException {
if (acceptor != null) {
try {
acceptor.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
</code></pre><p>这里的ServerSocket中,init0初始化socket监听,start0开始获取socket连接, destroy0销毁socket监听。<br>在这套生命周期管理机制下,我们将会很容易地对资源进行管理,不会发生资源未关闭的情况,架构和模块化更加清晰。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Java IO 中的流与设计模式
https://segmentfault.com/a/1190000004255439
2016-01-06T01:08:03+08:00
2016-01-06T01:08:03+08:00
ytbean
https://segmentfault.com/u/ytbean
10
<p>原文链接:<a href="https://link.segmentfault.com/?enc=TuW%2ForoIib735grEuQwxGg%3D%3D.2t1V%2BSbIazj9K4DsSHWHpWfD61XSkZNbsmsEWfJjpNPxaOwnhFgZUFWz0JLo1lBK%2F6Ikd%2BHIi6YVv8wLLa%2FAhQ%3D%3D" rel="nofollow">《Java IO 中的流与设计模式》http://www.ytbean.com/posts/java-io-stream-and-design-pattern/</a></p><h2>流概述</h2><p>Java中,流是一种有序的字节序列,可以有任意的长度。从应用流向目的地称为输出流,从目的地流向应用称为输入流。</p><h2>Java的流族谱</h2><p>Java的<code>java.io</code>包中囊括了整个流的家族,输出流和输入流的谱系如下所示:</p><p><img src="/img/remote/1460000041527890" alt="image-20220310172311188" title="image-20220310172311188"></p><h2>InputStream和OutputStream</h2><p>InputStream和OutputStream分别是输入输出流的顶级抽象父类,只定义了一些抽象方法供子类实现。</p><p>在输出流OutputStream中,如果你需要向一个输出流写入数据,可以调用<code>void write(int b)</code>方法,这个方法会将b的低八位写入流中,高24位将会被自动忽略。如果想要批量写入数据呢,那么可以调用<code>void write(byte[] b) </code>方法将一个字节数组的内容全部写入流中,同时还有<code>void write(byte[] b, int off, int len)</code>可以让你指定从哪里写入多少数据。<br>如果你希望你向流中写入的数据能够尽快地输送到目的地,比如说文件,那么可以在写入数据后,调用<code>flush()</code>方法将当前输出流刷到操作系统层面的缓冲区中。不过需要注意的是,此方法并不保证数据立马就能刷到实际的物理目的地(比如说存储)。<br>使用完流后应该调用其<code>close()</code>方法将流关闭,流关闭时,将会先flush,后关闭。</p><p>在输入流InputStream中,可以通过<code>int read() </code>方法来从流中读取一个字节,批量读取字节可以通过<code>int read(byte[] b)</code>或者<code>int read(byte[] b, int off, int len)</code>来实现,这两个方法的返回值为实际读取到的字节数。如果需要重复读取流中某段数据,可以在读取之前先使用<code>void mark(int readlimit)</code>方法在当前位置做一个记号,之后通过<code>void reset()</code>方法返回到之前做标记的位置,不过在做这些标记操作之前,需要先通过<code>boolean markSupported()</code>方法来确定该流是否支持标记。如果对某些可预知的数据不感兴趣,可以使用<code>long skip(long n)</code>来调过一些流中的一些数据。</p><p>使用完流,无论是输入还是输出流,都要调用其<code>close()</code>方法对其进行关闭。</p><h2>装饰器模式</h2><p>装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。</p><p>这种设计模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。</p><p>以InputStream为例,它是一个抽象类:</p><pre><code class="java">public abstract class InputStream implements Closeable {
...
...
}</code></pre><p>并定义有抽象方法</p><pre><code class="java">public abstract int read() throws IOException;</code></pre><p>该抽象方法由具体的子类去实现,通过InputStream的族谱图可以看到,直接继承了InputStream,并且提供某一特定功能的子类有:</p><ul><li>ByteArrayInputStream</li><li>FileInputStream</li><li>ObjectInputStream</li><li>PipedInputStream</li><li>SequenceInputStream</li><li>StringBufferInputStream</li></ul><p>这些子类都具有特定的功能,比如说,FileInputStream代表一个文件输入流并提供读取文件内容的功能,ObjectInputStream提供了对象反序列化的功能。</p><p>InputStream这个抽象类有一个子类与上述其它子类非常不同,这个子类就是<strong>FilterInputStream</strong>,可参见上图中的InputStream族谱图。</p><p>翻开FilterInputStream的代码,我们可以看到,它内部又维护了一个InputStream的成员对象,并且它的所有方法,都是调用这个成员对象的同名方法。<br>换句话说,FilterInputStream它什么事都不做。就是把调用委托给内部的InputStream成员对象。如下所示:</p><pre><code class="java">public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
return in.read(b, off, len);
}
public long skip(long n) throws IOException {
return in.skip(n);
}
public int available() throws IOException {
return in.available();
}
public void close() throws IOException {
in.close();
}
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
public synchronized void reset() throws IOException {
in.reset();
}
public boolean markSupported() {
return in.markSupported();
}</code></pre><p>FilterInputStream的又有其子类,分别是:</p><ul><li>BufferedInputStream</li><li>DataInputStream</li><li>LineNumberInputStream</li><li>PushbackInputStream</li></ul><p>虽然从上面代码看FilterInputStream并没有做什么有卵用的事,但是它的子类可不同了,以BufferedInputStream为例,这个类提供了提前读取数据的功能,也就是缓冲的功能。可以看看它的read方法:</p><pre><code class="java"> public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}</code></pre><p>可以看到,当pos>=count时,意即需要提前缓冲一些数据的时候到了,那么就会调用fill()将缓冲区加满,以便后续读取。<em>由于本文只讨论io流的装饰器模式,所以关于具体实现细节将不会展开讨论,比如本文不会讨论fill()方法是如何实现的,在这里可以先将它当做一个黑盒子。</em></p><p>从这里可以开始感受到,BufferedInputStream就是一个装饰者,它能为一个原本没有缓冲功能的InputStream添加上缓冲的功能。</p><p>比如我们常用的FileInputStream,它并没有缓冲功能,我们每次调用read,都会向操作系统发起调用索要数据。假如我们通过BufferedInputStream来<strong>装饰</strong>它,那么每次调用read,会预先向操作系统多拿一些数据,这样就不知不觉中提高了程序的性能。如以下代码所示:</p><pre><code class="java">BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("/home/user/abc.txt")));</code></pre><p>同理,对于其它的FilterInputStream的子类,其作用也是一样的,那就是装饰一个InputStream,为它添加它原本不具有的功能。OutputStream以及家属对于装饰器模式的体现,也以此类推。</p><p>JDK中的io流的设计是设计模式中装饰器模式的一个经典示范,如果细心发现,JDK中还有许多其它设计模式的体现,比如说监听者模式等等。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Java IO 中的文件操作
https://segmentfault.com/a/1190000004245393
2016-01-04T17:20:00+08:00
2016-01-04T17:20:00+08:00
ytbean
https://segmentfault.com/u/ytbean
2
<p>原文链接:<a href="https://link.segmentfault.com/?enc=khYTT4R6yzDcNJvLm7YPpg%3D%3D.f1LCEFBg%2BszWtH2t3tuRArdiz%2B0fvLAD6qFewefDkRGOzdGp%2BL0ievmPSFJ%2FnwRo" rel="nofollow">《Java IO 中的文件操作》http://www.ytbean.com/posts/java-io-file/</a></p><h2>File</h2><blockquote>File类位于JDK的<code>java.io</code>这个包下。<br>一个File类既可以代表一个文件,也可以代表一个目录。</blockquote><h3>构造器</h3><p>要使用File,首先需要通过构造器构造它的一个实例</p><pre><code>File file1 = new File("/a/b");
File file2 = new File("C:\\a\\b.dat");</code></pre><p>构造File类需要给它指定一个路径,比如上面代码中的<code>/a/b</code>,<code>C:\\a\\b.dat</code>.<br>路径可以代表一个文件,也可以代表一个目录。</p><p>路径分隔符依据操作系统的不同而不同,在类Unix系统中,分隔符是<code>/</code>,而在windows操作系统中,分隔符是<code>\\</code>,如果在代码中以硬编码的方式写死了路径分隔符,那么代码的可移植性就不高。可以通过<code>File.separator</code>或者是<code>File.separatorChar</code>来获取当前操作系统的路径分隔符。</p><p>File的构造器中的路径参数也支持绝对路径和相对路径,像上面的代码用的是绝对路径。那么相对路径相对的是哪个路径呢?Java会默认采用<code>user.dir</code>作为当前路径,可以通过<code>System.getProperty("user.dir")</code>来得到这个路径,这个路径也是JVM启动时所在的路径。</p><p>File也提供了另外一种构造器:</p><pre><code>File(String parent, String child)
File(File parent, String child)
</code></pre><p>这两个构造器可以让你在构造文件或目录时指定它的父目录。</p><h3>路径与名字</h3><p>File类包含了诸多获取路径和路径名字的方法,这些方法看似差别不大却又别有洞天,可以通过下面几段代码来看看区别:</p><p>执行以下代码</p><pre><code class="java">File file = new File(".");
System.out.println("Absolute path = " + file.getAbsolutePath());
System.out.println("Canonical path = " + file.getCanonicalPath());
System.out.println("Name = " + file.getName());
System.out.println("Parent = " + file.getParent());
System.out.println("Path = " + file.getPath());
System.out.println("Is absolute = " + file.isAbsolute());</code></pre><p>得到的结果是:</p><pre><code class="java">Absolute path = C:\prj\books\io\ch02\code\PathInfo\.
Canonical path = C:\prj\books\io\ch02\code\PathInfo
Name = .
Parent = null
Path = .
Is absolute = false</code></pre><p>执行以下代码:</p><pre><code class="java">File file = new File("C:\reports\2015\..\2014\February");
System.out.println("Absolute path = " + file.getAbsolutePath());
System.out.println("Canonical path = " + file.getCanonicalPath());
System.out.println("Name = " + file.getName());
System.out.println("Parent = " + file.getParent());
System.out.println("Path = " + file.getPath());
System.out.println("Is absolute = " + file.isAbsolute());</code></pre><p>得到的结果是:</p><pre><code class="java">Absolute path = C:\reports\2015\..\2014\February
Canonical path = C:\reports\2014\February
Name = February
Parent = C:\reports\2015\..\2014
Path = C:\reports\2015\..\2014\February
Is absolute = true</code></pre><p>执行以下代码:</p><pre><code class="java">File file = new File("");
System.out.println("Absolute path = " + file.getAbsolutePath());
System.out.println("Canonical path = " + file.getCanonicalPath());
System.out.println("Name = " + file.getName());
System.out.println("Parent = " + file.getParent());
System.out.println("Path = " + file.getPath());
System.out.println("Is absolute = " + file.isAbsolute());</code></pre><p>得到的结果是:</p><pre><code class="java">Absolute path = C:\prj\books\io\ch02\code\PathInfo
Canonical path = C:\prj\books\io\ch02\code\PathInfo
Name =
Parent = null
Path =
Is absolute = false</code></pre><p>从这里可以看出来,<code>file.getAbsolutePath()</code>会把相对路径的信息也打印出来,读起来并不是非常直观的,而<code>file.getCanonicalPath()</code>总是以对人类阅读友好的方式打印路径。<br>如果File的入参是绝对路径,那么<code>getName</code>和<code>getPath</code>只打印入参,并且<code>getParent</code>为null。</p><h3>得到文件/目录信息</h3><p>前面说过,File可以是一个文件,也可以代表一个目录,如何知道File代表的是哪一个呢?通过以下两个方法就可以知道</p><ul><li><code>boolean isDirectory()</code></li><li><code>boolean isFile()</code></li></ul><p>有时候我们想知道File代表的那个文件或目录是否在文件系统中存在,<code>boolean exists()</code>会告诉你。<br>在类Unix文件系统中,隐藏文件通常以<code>.</code>开头,比如用户的home目录下的<code>.bash_profile</code>,同样在windows中也会有隐藏文件,可通过<code>isHidden()</code>来判断一个文件是否是隐藏文件。通过<code>length()</code>可以获得文件的大小,通过<code>lastModified()</code>可以获得文件的最后修改时间,这个时间是距离(1970,1,1)的毫秒数。通常可以通过比较一个文件的最后修改时间来判断文件是否被修改过。</p><h4>列举某个目录</h4><p>可通过<code>File[] listRoots()</code>来列举当前文件系统的根目录。<br>在windows下,就是列出所有的盘符:</p><pre><code>C:\
D:\
E:\
F:\</code></pre><p>在Unix中,只有一个,那就是<code>/</code>。</p><p>如果要列出某个特定目录下的文件和目录呢,有以下方法:</p><pre><code class="java">String[] list()
String[] list(FilenameFilter filter)
File[] listFiles()
File[] listFiles(FileFilter filter)
File[] listFiles(FilenameFilter filter)</code></pre><p>以上方法中,返回<code>String[]</code>的,则是列举出所有文件或目录的名字。返回<code>File[]</code>的,则是所有文件或目录所代表的<code>File</code>对象。<br><code>FileFilter</code>和<code>FilenameFilter</code>是过滤器,能让你在列举目录时选择过滤掉哪些文件或目录。</p><h3>获取磁盘空间信息</h3><p>File提供了三个方法可以让你得知某个分区的磁盘空间的信息:</p><pre><code class="java">long getFreeSpace() //获取剩余空间
long getTotalSpace() //获取总空间大小
long getUsableSpace() //获取剩余可用空间</code></pre><p>尽管getFreeSpace和getUsableSpace看起来差不多,但实际上是有差别的,getUsableSpace会进行更多细致的检查,比如当前JVM进程是否对该目录有写权限,以及另外一些操作系统的限制等,但getFreeSpace和getUsableSpace返回的值只能当做一个参考值,因为有可能有其他的进程正在读写这个磁盘空间。<br>下面是一个例子:</p><pre><code class="java">File[] roots = File.listRoots();
for (File root: roots) {
System.out.println("Partition: " + root);
System.out.println("Free space on this partition = " +
root.getFreeSpace());
System.out.println("Usable space on this partition = " +
root.getUsableSpace());
System.out.println("Total space on this partition = " +
root.getTotalSpace());
System.out.println("***");
}</code></pre><p>输出结果为:</p><pre><code class="java">Partition: C:\
Free space on this partition = 143271129088
Usable space on this partition = 143271129088
Total space on this partition = 499808989184
***
Partition: D:\
Free space on this partition = 0
Usable space on this partition = 0
Total space on this partition = 0
***
Partition: E:\
Free space on this partition = 733418569728
Usable space on this partition = 733418569728
Total space on this partition = 1000169533440
***
Partition: F:\
Free space on this partition = 33728192512
Usable space on this partition = 33728192512
Total space on this partition = 64021835776
***</code></pre><h3>对文件或目录进行修改</h3><p>如果想创建一个文件,使用<code>boolean createNewFile()</code>将会创建一个新的空文件,同样,创建一个目录可以用<code>boolean mkdir() </code>或者<code>boolean mkdirs() </code>,如果中间目录不存在,后者会创建好所有中间目录,而前者将会报错某个目录不存在。</p><p>有时候你希望创建一个临时文件,可以使用<code>static File createTempFile(String prefix, String suffix)</code>,这个方法将会默认把临时文件放在用户的临时文件夹中,如果你想指定临时文件存放的地方,可以使用<code>static File createTempFile(String prefix, String suffix, File directory)</code>指定该目录。</p><h3>文件权限</h3><p>从Java 1.6开始,增加了对文件权限修改的接口。</p><pre><code class="java">boolean setExecutable(boolean executable)
boolean setExecutable(boolean executable, boolean ownerOnly)
boolean setReadable(boolean readable)
boolean setReadable(boolean readable, boolean ownerOnly)
boolean setWritable(boolean writable)
and boolean setWritable(boolean writable boolean ownerOnly)</code></pre><p>同时提供以下接口获取文件权限信息:</p><pre><code class="java">boolean canRead()
boolean canWrite()
boolean canExecute()</code></pre><h2>RandomAccessFile</h2><p>对文件的读取,既可以按顺序,也可以以任意顺序来读取。<br>RandomAccessFile提供这样一种功能。其保存一个指向当前文件位置的指针,可以通过调整指针的位置,读取一个文件中任意的内容。通过一段简单的代码来有个大体的认识:</p><pre><code class="java">RandomAccessFile raf = new RandomAccessFile("abc.log", "r");
int logIndex = 10;
raf.seek(logIndex);
//接下来通过raf进行文件操作</code></pre><h3>构造器</h3><p>RandomAccessFile提供了两个构造器</p><pre><code class="java">RandomAccessFile(File file, String mode)
RandomAccessFile(String path, String mode)</code></pre><h3>模式</h3><p>通过RandomAccessFile打开一个文件需要指定打开的模式,构造参数中的mode有四种模式可以选择:</p><ol><li>"r",以只读的方式打开一个已存在的文件,不可对文件进行写操作。</li><li>"rw",以读写的方式打开一个已存在的文件,若文件不存在,则创建一个,可对该文件进行读写操作。</li><li>"rwd",除了具有"rw"的特点外,这个模式要求对文件内容的每一个更新都会同步更新至底层的物理存储。</li><li>"rws",除了具有"rw"的特点外,这个模式要求对文件内容和文件<strong>元数据</strong>的每一个更新都会同步更新至底层的物理存储。</li></ol><blockquote>文件的元数据并非指文件的内容本身,文件的大小以及文件的最后修改时间等等算是元数据的一部分</blockquote><p>显然,如果指定了<strong>rwd</strong>或<strong>rws</strong>模式,那么对于文件的操作将会相对比较慢一些。</p><h3>读写</h3><p>RandomAccessFile内部维护了一个指针,指向当前读取或者写入的位置,当通过RandomAccessFile打开一个已存在的文件或者创建一个新文件时,指针自动指向下标为0的位置。进行写入操作时,如果指针已经指向文件的末尾,那么文件的大小将会被扩大。</p><p>当需要进行读取或者写入时,首先通过<code>void seek(long pos)</code>将文件的指针指向你想要读取或写入的位置,读取时有以下常用的方法可以进行读取:</p><ul><li><code>int read() //读取下一个字节</code></li><li><code>int read(byte[] b) //将读取的字节装入b数组中</code></li><li><code>char readChar() //读取两个字节,并将其转型为char类型</code></li><li><code>int readInt() //读取四个字节,并将其转型为int类型</code></li></ul><p>写入时有以下方法:</p><ul><li><code>void write(int b) //将b的低八位写入 </code></li><li><code>void writeChars(String s) //将字符串s所代表的字节写入</code></li><li><code>void write(byte[] b) //将字节数组b写入</code></li><li><code>void writeInt(int i) //写入4个字节的i</code></li></ul><p>除了读取写入的方法外,<code>setLength(long newLength)</code>方法可以设置文件的大小,如果newLength小于当前文件大小,那么文件将会被截肢,反之,文件将会被扩大到newLength。</p><h3>FileDescriptor</h3><p>值得注意的是,RandomAccessFile提供了一个<code>FileDescriptor getFD()</code>方法获取文件所对应的文件描述符对象,文件描述符代表是一种平台独立的文件描述结构,通过这个描述符可以对文件进行一些特殊的操作。</p><p>FileDescriptor定义了<code>sync()</code>方法,与之前提到的"rwd"和"rwd"一样,sync方法用来告诉操作系统将缓冲区的内容全部刷到物理的存储上。如果没有指定rwd或者rws模式,那么对文件的写入将会暂时存储于操作系统层面的缓冲区里面,当缓冲区满时,操作系统才会将内容刷至物理磁盘,通过sync()方式可以让操作系统对每一次写入操作都同步刷新至物理存储中,以下为一个例子:</p><pre><code class="java">RandomAccessFile raf = new RandomAccessFile("abc.log", "rw");
//这里的模式不是rwd或者rws
FileDescriptor fd = raf.getFD();
raf.write(...);
// 通过fd的sync方法,可以让写入操作同步地刷新至物理存储
fd.sync();
raf.close();</code></pre><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Linux RPM包管理机制详解
https://segmentfault.com/a/1190000003047674
2015-08-03T18:33:07+08:00
2015-08-03T18:33:07+08:00
ytbean
https://segmentfault.com/u/ytbean
0
<p>原文链接:<a href="https://link.segmentfault.com/?enc=ZsUPzTitwfgoFbv7Qlm7yw%3D%3D.pPIXhmyeSJzvdb7rCzJcj%2FcSAVWBLvmL9AqiZqjBBLlgWu1SyOWaPUEwWAQsounKQRw7qWye09XRC4osppIagw%3D%3D" rel="nofollow">《Linux RPM包管理机制详解》http://www.ytbean.com/posts/linux-rpm-internals/</a></p><h2>RPM 简介</h2><h3>什么是包(Packages),为什么要管理它们</h3><p>要回答这个问题,我们需要回到三个最基本的问题上面来:</p><ul><li>计算机</li><li>数据</li><li>程序</li></ul><p>计算机需要获取数据和程序来做它应当做的事情,把数据和程序交给计算机,意味着把它们放进计算机的大容量存储里,现在,这又意味着放进硬盘里。数据和程序将会在硬盘里以文件的形式被存储。</p><p>而数据,数据不仅需要空间去存储它,更重要的是,它需要以程序能处理的格式存储。</p><p>最后,谈一谈程序,程序和数据一样,也需要一定的存储空间,但对于程序来说,以下几点更为重要:</p><ul><li>程序可能需要数据才能运行,这些数据必须格式正确、命名规范,并且存储在硬盘中某个合适的位置以便程序能够访问它。</li><li>程序可能需要配置文件,这些配置文件能够控制程序的行为,使程序表现出定制化的行为。</li><li>程序在硬盘上也需要工作空间,其次,程序也像数据一样,需要有一个合适的命名,并且存放在硬盘中合适的位置。</li><li>程序也有可能需要依赖其他的程序。</li><li>尽管一个程序的正常运行并不需要文档参与,但带有说明文档的程序,能以人类容易阅读的方式让使用者了解程序的使用方法。</li></ul><p>现在想一想,当我们需要在电脑上安装软件时,我们可能采取的方式可能有以下两种:</p><ol><li>阅读程序的文档,把程序,配置文件,以及数据拷贝到你的电脑上,确保它们的命名规范,并且放在了硬盘上的合适位置,而且,硬盘有足够的空间来放下这些东西。接下来,按你的意愿修改一下配置文件,最后,运行程序。</li><li>让电脑为你做这些事。</li></ol><p>如果你觉得第一种方式还OK啊,可以接受。但是你有没有想过你需要同时追踪多少个文件,在Linux系统中,一个程序有超过两万个文件是很正常的事情,有大量的文档需要你去阅读,大量的文件需要拷贝,还有配置。而且,当你想要更新软件版本的时候,你要怎么办?凡此种种,不一而足。</p><p>有些人会觉得第二种方式最简单啦:让电脑为你做这些事。RPM的出现,就是为了满足这些人的期待!</p><h3>走进“包(Packages)”</h3><p>计算机能像赶鸭子一样,很好地管理2万个以上的文件,这也是包管理软件擅长做的。不过,说了这么久,到底什么是包?</p><p>计算机眼中的包和我们日常生活中见到的包,其实是相似的,它们都能够把一些相关的东西放在同一个地方。在它们被使用前,它们都需要先被“打开”,在包上可以贴一个标签,以说明它里面装的是什么东西。</p><p>一般,包管理系统会把各种不同的文件,包括程序,数据,文档和配置信息,全部打包在一个特定格式的文件里,这个文件就叫一个包文件。以RPM为例,这个包文件叫做“package”,“.rpm文件”,或者直接就叫“RPM”,名字不同,但其实代表的是一样东西。一个包,包含了RPM安装所需要的所有东西。</p><p>一个rpm包通常包含下面这些类型的软件:</p><ul><li>一系列只有单个任务的程序集合,通常也叫作“应用(Application)”,比如说字符处理程序,或者是一门编程语言。</li><li>操作系统的一个特定部分,例如,操作系统启动时的初始化脚本,或一个特别的命令行SHELL,或者是一个支持web服务器的软件。</li></ul><h4>使用包的好处</h4><p>使用包的一个最明显的好处是包是作为一个整体被管理的,如果需要移动该包,只需要移动整个包,而不用担心会漏掉某些文件,尽管这是一个最明显的好处,但是却不是最大的好处。</p><p>使用包的最大的好处是,包本身携带了它应该如何被安装的信息。不仅可以携带安装的步骤信息,也可以携带卸载所需要的步骤信息。</p><h3>管理你的包吧,不然你会被它管理。</h3><p>尽管包的使用已经降低了软件安装的复杂度,但它还是做不到不用你任何的参与就能做到安装,卸载。跟踪哪些包已经安装在你的系统上了,这是件很必要的事情,特别是当你所要安装的包依赖于其他包时。</p><h4>包的管理也需要你动手</h4><p>你会发现,你对包的管理很可能就是在做以下这些事:</p><ul><li>安装新的包。</li><li>更新包。</li><li>卸载包。</li></ul><p>你需要总是做这些事情,也会很容易就无法对包的信息进行跟踪和掌握。你应该知道些什么有关于包的信息呢?</p><h4>跟踪并管理包</h4><ul><li>很显然,你一定十分渴望看看在自己的操作系统上有哪些包已经安装了。</li><li>如果能获得你指定的某个包的信息,那就更好了。这些信息包括了从包开始安装的时间开始到一大串文件被安装结束。</li><li>能够通过多种方式获得包的信息也是激动人心的,技能找出一个包安装了什么文件,也能找出哪些包安装了某个特定的文件。</li><li>也应该能够查看一个包现在的安装方式和它之前的安装方式有何不同,误删文件是一件很平常的事,如果包管理器能够告诉你缺失了哪些文件,那么就能让你的软件正常运作起来。</li><li>配置文件包含的信息也让人很头疼,如果能够多注意这些配置文件,保证配置的改变不会丢失,那么,生活就会更愉悦了。</li></ul><h3>包管理,应该怎么做?</h3><p>前文为你描述了一个美好的愿景:包管理,能够让你更容易安装、更新和删除包;以多种方式查看包的信息;确保正确安装了包;甚至追踪配置文件的改动。但是,你要怎么做到这些呢?</p><p>前文也已经提过,最好的方式去做这些事就是让你的电脑帮你做。很多公司和组织已经开发了包管理系统。包管理系统主要有两种实现方式:</p><ol><li>一些包管理系统关注的是使用包的步骤。</li><li>另外一些包管理系统关注的是包所涉及的文件,并对这些文件进行修改追踪。</li></ol><p>这两种实现方式有各自的优势,但也有各自的缺点。第一种方式,能更容易地安装新的包,但是删除旧包很困难,而且,几乎不可能得到任何关于已安装包的有意义的信息。</p><p>第二种方式得到已安装包的有用信息很容易,安装和删除包也比较容易。但这方式一个最不好的地方在于它无法在安装或删除包时执行一些特别的命令。</p><p>而实际上,没有一个包管理系统使用单独一种方式来实现,都是两种方式的组合实现,</p><h3>RPM的设计目标</h3><p>RPM的设计目标可以用一句话来概括:"something for everyone",尽管RPM存在的主要原因是Red Hat公司为了更方便地在它们的linux发行版中安装几百个包,但这并不是RPM存在的唯一理由。我们可以看看Red Hat公司在设计RPM时的需求:</p><h4>使用安装和卸载更容易</h4><p>正如我们在前文中可以看到的那样,安装一个包需要很多复杂的步骤。如果把这些事情交给人类来做,失败率是很高的。因此RPM的目的就是要帮助人们更简单地安装包,卸载包。</p><p>另一方面,RPM又需要给包的创建者足够的控制权让他们来控制这个包应该怎样安装。原因很简单:只要包的创建者多下一些功夫,那么这些包就能很好的安装和卸载。</p><h4>使检验一个包是否正确安装更容易。</h4><p>软件就像生活一样,总有许多意想不到的问题,RPM应该能够捕获到这些问题,比如说文件缺失或者文件非法修改。</p><h4>使包的创建者创建包的时候更容易</h4><p>节约了包创建者的很多时间和精力。</p><h4>使包从源代码开始构建。</h4><p>站在包创建者的角度上看,使RPM从源代码开始工作十分重要。为什么呢?</p><p>使用源代码,可以让他们把为了修改bug、添加新功能等等而添加的修改与之前的修改区分开来,这对于包构建者来说是一件好事,因为他们很多人不是软件的源作者。</p><p>把修改区分开,这样即使在几个月以后,依然能够明确地知道对包有过哪些改动</p><h4>使其平台独立</h4><p>包创建者需要做的一件繁杂的事就是使程序能够在不同类型的电脑上运行,RPM能够得到程序的源代码,加上一些必要的修改使其能够被正确构建,这是非常方便的。</p><h3>包里面有什么东西</h3><h4>包名片</h4><p>每个RPM包都有一些信息,用来作为自身的唯一标识。我们把这些信息叫做一个包名片,下面是两个包名片的样本例子:</p><ul><li>nls-1.0-1</li><li>perl-5.001m-4</li></ul><p>尽管这些名片看起来似乎是十分不同的,但是他们都遵循RPM包的包名片命名规则。每个包名片由以下三部分按顺序组成:</p><h5>组成部分1:软件的名字</h5><p>每个包名片以软件的名字开始,例如上例中,分别为nls和perl。</p><h5>组成部分2:软件的版本</h5><p>例如上例中,版本号分别为1.0和5.001m。</p><h5>组成部分3:包的发布号</h5><p>包的发布号反映出包在同一个版本号下重新构建的次数,重新的构建的原因可能是因为修复了一个Bug。习惯上,发布号从1开始。上例中的发布号分别为1和4。</p><h4>与包有关的信息</h4><ul><li>包被创建的日期和时间。</li><li>对包的内容的说明。</li><li>包所需要安装的文件的总数。</li><li>分组的信息。</li><li>签名信息。</li></ul><h4>包中的文件信息</h4><ul><li>每个文件的名字以及它将要安装到哪个目录。</li><li>每个文件的权限。</li><li>文件的所有者以及所属的用户组</li><li>每个文件的MD5校验码</li><li>文件的内容</li></ul><h2>安装软件</h2><h3>rpm -i 它做了什么</h3><p>一说到rpm,人们第一时间想到的就是rpm能用来安装软件。正如我们前面提及到的,安装软件时一个复杂的,经常出错的事情,但在rpm眼中,安装软件只不过是一个命令的事。</p><p><strong>rpm -i (等同于rpm --install)</strong> 命令能够安装已经被打包成rpm格式的软件,它主要做以下几件事:</p><ul><li>依赖检查.</li><li>检测冲突</li><li>做一些正式安装前必须做的准备工作</li><li>根据配置文件确定如何安装软件</li><li>解压包并把它们放在一个合适的路径下</li><li>执行一些在安装后须要做的工作</li><li>对它自身的所作所为进行跟踪</li></ul><p>下面将逐个解释上述所述的几点:</p><h4>依赖检查</h4><p>有时候,一些安装包须要在它所依赖的安装包安装好了之后才能正常安装。RPM将会确认所需安装软件的依赖包已经安装好了,它也会保证安装软件包时不影响其它已经安装好的软件。</p><h4>检测冲突</h4><p>RPM在这个阶段将会进行一系列检测,如果试图安装一个已经安装过的软件,或者用旧版本覆盖新版本的软件,或者是非法改写某个已安装软件的软件。这些RPM都能检测出来并及时制止。</p><h4>做一些正式安装前必须做的准备工作</h4><p>一些命令必须在正式软件安装开始之前优先执行,RPM将会执行你所定义的这些命令,这样能够避免在安装时遇到很多问题。</p><h4>根据配置文件确定如何安装软件</h4><p>RPM与其他包管理软件不同的一点是,它会使用配置文件,尽管有时候改变配置文件只是为了个性化地安装软件,但这会激怒你的小伙伴,因为他们之前所做的一些个性化配置全都没了。而RPM会分析配置文件,并尝试去做正确的事,即使这些软件一开始并不是使用RPM安装的。</p><h4>解压包并把它们放在一个合适的路径下</h4><p>每个安装包都会包含许多待安装的文件,并且包含了每个文件需要被安装到哪个目录下,而且,文件的其他一些属性,例如权限和所有者,RPM都会进行正确的设置。</p><h4>执行一些在安装后须要做的工作</h4><p>有时候,需要在软件安装后执行一些命令。比如说,执行ldconfig命令来使一些库变成公用的。</p><h4>对它自身的所作所为进行跟踪</h4><p>每当RPM把软件安装到你的系统上后,它会在数据库中保留对文件的跟踪,数据库中存储了大量有用的信息,例如,当RPM检测冲突时,它就会使用到它存储在数据库中的信息。</p><h3>RPM安装实战</h3><p>让我们来看看一个例子,安装一个软件,你只需要使用命令<code>rpm -i</code>,命令后跟着rpm包文件就可以:<br><code> # rpm -i eject-1.2-2.i386.rpm</code></p><p>这时候,上文中所提到的几件事情在这个时候已经完成了。这个软件包已经被安装好了,需要注意的是,这里的安装包文件并不需要严格遵守rpm包的命名规范,例如:<br><code># mv eject-1.2-2.i386.rpm baz.txt</code><br><code># rpm -i baz.txt</code></p><p>在这个例子中,我们把rpm包的名字从eject-1.2-2.i386.rpm改为了baz.txt,安装结果将会和之前的安装结果的一样。rpm包的名字在RPM进行安装时,将不会被使用。RPM用的是包里的文件的内容,无论名字怎么改,RPM始终都能读取包里的文件来实现正确安装。</p><h4>使用URL来指定包文件</h4><p>如果你上网,你一定会注意到一个网页是这样被标识的:</p><pre><code>http://www.redhat.com/support/docs/rpm/RPM-HOWTO/RPM-HOWTO.html
</code></pre><p>这叫做一个统一资源定位符(Uniform Resource Locator),或者叫URL,RPM也可以使用URL来安装软件,尽管URL看起来有些不一样,下面有另外一个例子:</p><pre><code># ftp://ftp.redhat.com/pub/redhat/code/rpm/rpm-2.3-1.i386.rpm
</code></pre><p>ftp标识着这个URL是基于文件传输协议的,正如名字所暗示的那样,这个类型的URL是用来传输文件的。</p><p>RPM对URL的支持使我们能够通过一个简单的命令来安装软件:</p><pre><code># rpm -i ftp://ftp.gnomovision.com/pub/rpms/foobar-1.0-1.i386.rpm
</code></pre><h4>也许你会看到你从未见过的警告信息</h4><p>依环境而定,当你在安装一个普通软件的时候,以下信息你可以从来没有见到过或者是经常见到过:</p><p><code># rpm -i cdp-0.33-100.i386.rpm</code></p><p><code>warning: /etc/cdp-config saved as /etc/cdp-config.rpmorig</code></p><p>这是什么意思,这要从RPM处理配置文件说起,在上面的例子中,RPM找到了一个文件(/etc/cdp-config),这个文件不属于任何已经安装了的包,由于cdp-0.33-100包含了一个与该文件同名的包,并且该文件要安装在同一个目录下,就会遇到上面这个警告信息。</p><p>RPM将会有两个步骤来处理这个问题:</p><ol><li>把原来的文件改名为cdp-config.rpmorig.</li><li>使用安装包中的cdp-config来安装软件。</li></ol><p>接下来我们查看这个目录,我们会看到一切如我们所说的那样发生了:</p><pre><code> # ls -al /etc/cdp*
-rw-r--r-- 1 root root 119 Jun 23 16:00 /etc/cdp-config
-rw-rw-r-- 1 root root 56 Jun 14 21:44 /etc/cdp-config.rpmorig</code></pre><h3>两个有用的选项</h3><p>有两个选项,能够帮助rpm -i 工作得更好,也很有用。你也许会意味它们是RPM的默认行为,但实际上不是,只不过要使用它们,你得多打一些字:</p><h4>使用 -v 选项得到更多的反馈</h4><p>尽管rpm -i 已经做了很多事情,但是还不够,不是吗? 当进行安装的时候,rpm表现得太安静了,除非安装过程中出了错。不过我们可以通过加上 -v 选项来让它输出更多的信息:</p><pre><code># rpm -iv eject-1.2-2.i386.rpm
Installing eject-1.2-2.i386.rpm
</code></pre><p>使用 -v 好处是很多的,特别是当你需要用一行命令来安装多个软件的时候:</p><pre><code># rpm -iv *.rpm
Installing eject-1.2-2.i386.rpm
Installing iBCS-1.2-3.i386.rpm
Installing logrotate-1.0-1.i386.rpm
</code></pre><h4>无耐心者的福音 -h</h4><p>有时候一个安装包可能非常大,除了呆呆地看着硬盘的灯在闪,你找不到其他方式知道RPM的工作进度,还要多久才能安装完。加上-h选项,RPM会打印出<strong>#</strong>来显示进度,50个<strong>#</strong>的出现意味着安装完成。</p><pre><code># rpm -ih eject-1.2-2.i386.rpm
##################################################
</code></pre><p>一旦50个<strong>#</strong>出先了,那么就代表软件已经完成,这一点在你安装多个软件时也很有用:</p><pre><code># rpm -ivh *.rpm
eject ##################################################
iBCS ##################################################
logrotate ##################################################
</code></pre><h3>更多rpm -i 的选项</h3><ul><li>-vv:得到更多的信息</li><li>--test:只进行安装测试</li><li>--replacepkgs:覆盖安装</li><li>--replacefiles:即使覆盖了其他软件的文件,也照常安装</li><li>--nodeps:安装前不做依赖检查</li><li>--force:无论怎样,都给老子安装</li><li>--excludedocs:不安装文档</li><li>--includedocs:安装文档</li><li>--prefix <path>:重定向安装包路径为<path></li><li>--noscripts:不执行安装前后的脚本命令</li><li>--percent:显示安装进度的百分比</li><li>--rcfile <rcfile>:使用<rcfile>作为备选的rcfile</li><li>--root<path>:使用<path>作为备选的root</li><li>--dbpath <path>:使用<path>来访问数据库</li><li>--ftpport <port>:使用<port>所指定的端口来执行基于FTP协议的安装</li><li>--ftpproxy <host>:使用<host>所指定的地址作为FTP代理</li><li>--ignorearch:不校验安装包的格式</li><li>--ignoreos:不检查安装包的操作系统信息</li></ul><h3>rpm -e 做了什么</h3><p>rpm -e(等同于 rpm --erase)这个命令能够卸载或擦除一个或多个安装包,当RPM卸载一个RPM包时,做了以下几件事:</p><ul><li>确保数据库中没有其它包引用了要卸载的包。</li><li>执行卸载前的脚本(如果有的话)</li><li>检查配置文件是否已经被修改过,如果是,则保留它们的一个备份。</li><li>查询数据库,找到这个包安装的所有文件,如果该些文件不属于别的包,则将它们删除。</li><li>执行卸载后的脚本(如果有的话)</li><li>从数据库中删除包的所有追踪信息。</li></ul><h3>卸载一个包</h3><pre><code># rpm -e eject
</code></pre><p>这样,eject包就被无声无息地卸载了,显然我们会想要得到更多的反馈信息,加上-v选项试试:</p><pre><code># rpm -ev eject
</code></pre><p>依然是没有任何东西输出,但是还有一个选项我们可以用。见下文。</p><h4>使用-vv得到更多反馈信息</h4><p>通过加上-vv选项,我们可以得到RPM卸载过程中的更多反馈信息:</p><pre><code># rpm -evv eject
D: uninstalling record number 286040
D: running preuninstall script (if any)
D: removing files test = 0
D: /usr/man/man1/eject.1 - removing
D: /usr/bin/eject - removing
D: running postuninstall script (if any)
D: removing database entry
D: removing name index
D: removing group index
D: removing file index for /usr/bin/eject
D: removing file index for /usr/man/man1/eject.1
</code></pre><p>虽然-v无法告诉我们什么东西,但是-vv却告诉我们很多东西,不过,它究竟告诉了我们什么呢?</p><p>首先,RPM打印出了软件包的记录号,这个记录号只对于那些写RPM数据库代码的人才有意义。</p><p>接着,RPM执行卸载前脚本,如果有脚本的话。</p><p>"removes files test = 0"这一行标识RPM将会卸载整个软件包,如果这个数字不为0的话,RPM只是进行了卸载环境的检测而已。当加上--test选项时,不为0的情况才会发生。</p><p>接下来的两行显示出了卸载过程中删除的文件,如果一个包中包含了很多文件,那么使用-vv参数将会导致大量的输出。</p><p>紧接着,RPM执行卸载后脚本,如果存在的话。这个脚本在所有文件删除后才执行。</p><p>最后,最后5行显示出RPM删除了数据库中的跟踪信息。</p><h3>其他选项</h3><ul><li>--test:做卸载环境检测,但并不真正卸载软件。</li><li>--nodeps:在卸载之前,不检查依赖关系</li><li>--noscripts:不执行卸载前或卸载后的脚本</li><li>--rcfile :使用<rcfile>作为备选的rcfile</li><li>--root:使用<root>作为备选的root</li><li>--dbpath :使用<dbpath>来访问数据库</li></ul><h3>rpm -e 与配置文件</h3><p>如果你修改了软件安装时的配置文件,那么即使你卸载了软件,配置信息依然不会丢失。例如,你修改了/etc/skel/.bashrc(一个配置文件),这个配置文件是作为etcskel包的一部分被安装的。接下来,我们删除etcskel:</p><pre><code># rpm -e etcskel
</code></pre><p>我们去/etc/skel目录下看看:</p><pre><code># ls -al
total 5
drwxr-xr-x 3 root root 1024 Jun 17 22:01 .
drwxr-xr-x 8 root root 2048 Jun 17 19:01 ..
-rw-r--r-- 1 root root 152 Jun 17 21:54 .bashrc.rpmsave
drwxr-xr-x 2 root root 1024 May 13 13:18 .xfm
</code></pre><p>很显然,.bashrc.rpmsave这个文件就是你修改的配置的一个备份,然而你也应当要知道的是,这只是对配置文件RPM才会保留一个备份。</p><h3>请注意</h3><p>RPM卸载软件时几乎替你在操作系统上做了所有的事,这很棒。但是,这也意味着RPM在卸载你系统上的重要软件时,也一样铁面无私。例如:</p><ul><li>RPM: RPM能卸载它自己吗,答案当然是可以。</li><li>Bash: 当心卸载掉了你机子上的Bash。</li></ul><p>大多数情况下,RPM的依赖检测能检测到你所需要卸载的软件与其他软件的依赖关系,这会提醒你不要误删了软件。如果你是在不确定有何依赖关系,可以使用rpm -q来查询你想要卸载的软件。</p><h2>卸载软件</h2><h3>rpm -e 做了什么</h3><p>rpm -e(等同于 rpm --erase)这个命令能够卸载或擦除一个或多个安装包,当RPM卸载一个RPM包时,做了以下几件事:</p><ul><li>确保数据库中没有其它包引用了要卸载的包。</li><li>执行卸载前的脚本(如果有的话)</li><li>检查配置文件是否已经被修改过,如果是,则保留它们的一个备份。</li><li>查询数据库,找到这个包安装的所有文件,如果该些文件不属于别的包,则将它们删除。</li><li>执行卸载后的脚本(如果有的话)</li><li>从数据库中删除包的所有追踪信息。</li></ul><h3>卸载一个包</h3><pre><code># rpm -e eject
</code></pre><p>这样,eject包就被无声无息地卸载了,显然我们会想要得到更多的反馈信息,加上-v选项试试:</p><pre><code># rpm -ev eject
</code></pre><p>依然是没有任何东西输出,但是还有一个选项我们可以用。见下文。</p><h4>使用-vv得到更多反馈信息</h4><p>通过加上-vv选项,我们可以得到RPM卸载过程中的更多反馈信息:</p><pre><code># rpm -evv eject
D: uninstalling record number 286040
D: running preuninstall script (if any)
D: removing files test = 0
D: /usr/man/man1/eject.1 - removing
D: /usr/bin/eject - removing
D: running postuninstall script (if any)
D: removing database entry
D: removing name index
D: removing group index
D: removing file index for /usr/bin/eject
D: removing file index for /usr/man/man1/eject.1
</code></pre><p>虽然-v无法告诉我们什么东西,但是-vv却告诉我们很多东西,不过,它究竟告诉了我们什么呢?</p><p>首先,RPM打印出了软件包的记录号,这个记录号只对于那些写RPM数据库代码的人才有意义。</p><p>接着,RPM执行卸载前脚本,如果有脚本的话。</p><p>"removes files test = 0"这一行标识RPM将会卸载整个软件包,如果这个数字不为0的话,RPM只是进行了卸载环境的检测而已。当加上--test选项时,不为0的情况才会发生。</p><p>接下来的两行显示出了卸载过程中删除的文件,如果一个包中包含了很多文件,那么使用-vv参数将会导致大量的输出。</p><p>紧接着,RPM执行卸载后脚本,如果存在的话。这个脚本在所有文件删除后才执行。</p><p>最后,最后5行显示出RPM删除了数据库中的跟踪信息。</p><h3>其他选项</h3><ul><li>--test:做卸载环境检测,但并不真正卸载软件。</li><li>--nodeps:在卸载之前,不检查依赖关系</li><li>--noscripts:不执行卸载前或卸载后的脚本</li><li>--rcfile :使用<rcfile>作为备选的rcfile</li><li>--root:使用<root>作为备选的root</li><li>--dbpath :使用<dbpath>来访问数据库</li></ul><h3>rpm -e 与配置文件</h3><p>如果你修改了软件安装时的配置文件,那么即使你卸载了软件,配置信息依然不会丢失。例如,你修改了/etc/skel/.bashrc(一个配置文件),这个配置文件是作为etcskel包的一部分被安装的。接下来,我们删除etcskel:</p><pre><code># rpm -e etcskel
</code></pre><p>我们去/etc/skel目录下看看:</p><pre><code># ls -al
total 5
drwxr-xr-x 3 root root 1024 Jun 17 22:01 .
drwxr-xr-x 8 root root 2048 Jun 17 19:01 ..
-rw-r--r-- 1 root root 152 Jun 17 21:54 .bashrc.rpmsave
drwxr-xr-x 2 root root 1024 May 13 13:18 .xfm
</code></pre><p>很显然,.bashrc.rpmsave这个文件就是你修改的配置的一个备份,然而你也应当要知道的是,这只是对配置文件RPM才会保留一个备份。</p><h3>请注意</h3><p>RPM卸载软件时几乎替你在操作系统上做了所有的事,这很棒。但是,这也意味着RPM在卸载你系统上的重要软件时,也一样铁面无私。例如:</p><ul><li>RPM: RPM能卸载它自己吗,答案当然是可以。</li><li>Bash: 当心卸载掉了你机子上的Bash。</li></ul><p>大多数情况下,RPM的依赖检测能检测到你所需要卸载的软件与其他软件的依赖关系,这会提醒你不要误删了软件。如果你是在不确定有何依赖关系,可以使用rpm -q来查询你想要卸载的软件。</p><h2>升级软件</h2><h3>rpm -U 做了什么</h3><p>如果RPM的命令中有一条命令好用到没朋友,那么这条命令就是RPM的软件升级命令了。毕竟,只有那些尝试过手动在linux中升级一个软件的版本的人才知道蛋蛋有多疼。有了RPM,软件升级只不过是一个命令的事:rpm -U(等同于rpm --upgrade)。这个命令执行了一下两个独立的操作:</p><ol><li>安装软件的升级版。</li><li>卸载旧版本。</li></ol><p>如果你天真地说rpm -U不算什么,完全可以用rpm -i和rpm -e来替换,那么,我会告诉你,你真的说对了。<br>也许有人会认为rpm -U只是一个鸡肋,因为它只是执行了其它命令的组合而已。事实上,rpm -U这样做自有它的机灵之处。rpm -U精心设计了安装和卸载命令的组合,使得即使升级不成功时,依然能够保护重要的文件不被删除。</p><p>单独地使用安装和卸载的命令,并不能胜任软件升级的任务,安装意味着可能会覆盖一个修改过的配置文件,同样,卸载意味着可能会删除配置文件。而使用升级命令,即使你的电脑上安装了该软件的多个版本,也能顺利升级。</p><h4>配置文件的魔法</h4><p>虽然rpm -i 和rpm -e各自都对配置文件的操作,但是,使用rpm -U才能真正保护好配置文件。当RPM在处理配置文件时,它至少考虑了六种可能发生的场景。</p><p>为了做出正确的决定,RPM需要获得一些信息。这些信息是什么呢,它就是配置文件的<strong>3</strong>个MD5校验码,每次配置文件的更改都会产生一个不同的MD5校验码,MD5校验码不同,则说明两个配置的内容不同。</p><p>我们特意提到是<strong>3</strong>个MD5校验码,那么这<strong>3</strong>个校验码分别是什么呢:</p><ol><li>软件安装时的<strong>原始配置文件</strong>的MD5校验码</li><li>软件升级曾经升级过的<strong>当前配置文件</strong>的MD5校验码</li><li>新的软件升级包中的<strong>新配置文件</strong>的MD5校验码</li></ol><p>上面三种不同的MD5校验码的组合,决定RPM在升级软件时的行为。下文中,将会使用X,Y,Z来代替MD5校验码。</p><h5>原始配置文件=X,当前配置文件=X,新配置文件=X</h5><p>这种场景下,一开始安装的配置文件安装后就一直没有修改过,如果RPM安装了新的配置文件,将会覆盖旧的配置文件。有时候你也许会感到奇怪,我新安装的文件它的内容和名字我都没有修改过,为什么还要覆盖呢,这是因为新版本的文件的属性可能已经修改过了,比如文件的所有者。</p><h5>原始配置文件=X,当前配置文件=X,新配置文件=Y</h5><p>原始配置并没有被修改过,但新的安装包中它的配置文件与当前配置文件却是不同的,这些改变可能是为了修改一个Bug,或者新添一个新功能。</p><p>在这种情况下,RPM将会安装新的配置文件,并覆盖旧的配置文件。RPM并不会为原先的配置文件保留一个备份,因为在升级软件之前,它一直都没有改动过。</p><h5>原始配置文件=X,当前配置文件=Y,新配置文件=X</h5><p>配置文件被修改过了,但是,新的安装包中的配置文件却和原始的配置文件是一样的。</p><p>这种情况下,RPM会认为由于新的安装包中配置文件和原始配置文件的内容是一样的,而当前配置文件虽然已被修改过,但它认为这些修改对于新的版本来说依然是合法的。于是它不会去覆盖当前的配置文件。</p><h5>原始配置文件=X,当前配置文件=Y,新配置文件=Y</h5><p>配置文件被修改过了,并且修改的恰恰与新的安装包中的配置文件中的修改是一样的。</p><p>这种情况下,RPM将会安装新版本,并且覆盖当前配置文件,这和第一种情况的处理方式是一样的。尽管新的配置文件和当前配置文件相比,但是文件的其他属性可能修改了,因此安装新版本配置文件。</p><h5>原始配置文件=X,当前配置文件=Y,新配置文件=Z</h5><p>在这里,原始配置文件已经被修改过了,并且新的配置文件和当前配置文件以及原始配置文件都是不同的。</p><p>RPM是无法通过分析配置文件中的内容来决定如何操作的。不过在这种情况下,RPM会选择一种它认为最好的方式来处理,新配置文件肯定是能兼容新的安装包的,当前配置文件,则可能兼容新的安装包,也可能不兼容。因此RPM会安装新的配置文件。</p><p>不过,当前配置文件由于被修改过,那一定是某个人有意为之的,有可能当前配置文件所做的修改对新版本的软件包依然适用,因此,RPM会保留当前配置文件的一个备份为<file>.rpmsave,并且打印出一条警告信息。</p><pre><code>warning: /etc/skel/.bashrc saved as /etc/skel/.bashrc.rpmsave
</code></pre><h5>原始配置文件=none,当前配置文件=??,新配置文件=??</h5><p>出现这种情况,是因为RPM在一开始安装软件的时候,并没有安装配置文件,因此没有MD5校验码。</p><p>因为没有原始文件的MD5,所以RPM无法决定当前配置文件是否被修改过的。因此,RPM会把当前文件保存一个备份,命名为<file>.rpmorig,然后打印一条警告信息,然后安装新的配置文件。</p><p>正如你所看到的,大多数场景下,RPM会采取最合适的方式来做升级操作。不过,大多数情况下,已经修改过的配置文件是不值得备份的,通常都会被删掉。</p><h3>升级软件包</h3><p>使用rpm -U的方式如下所示:</p><pre><code># rpm -U eject-1.2-2.i386.rpm
</code></pre><h4>rpm -U 见不得人的小秘密</h4><p>假设,我们用rpm -U升级一个软件,但实际上,这个软件从来没有安装过,既然没有安装过,谈何升级呢?这个时候,rpm -U就会变成rpm -i啦,相当于安装软件,而不是升级软件。</p><p>既然rpm -U能够表现出rpm -i的行为,因此很多时候,人们直接用一个命令rpm -U来安装和升级软件。</p><h3>它们只是几乎相同而已</h3><p>rpm -U在特殊情况下可以代替rpm -i,因此rpm -i的绝大多数附加命令选项对于rpm -U也是适用的。下面是一些rpm -U独有的选项:</p><ul><li>--oldpackage: 升级到旧版本</li><li>--force: 无论发生什么事,给老子升级</li></ul><h2>获取包信息</h2><h3>rpm -q 做了什么</h3><p>如果你想要在你的系统上安装、卸载或升级软件,但却不知道在你的系统中已经有哪些软件了,这是不是一件很蛋疼的事?你可能会陷入下面这些场景中:</p><ul><li>你在你的系统中遇到一个文件,你不认识它,它也不认识你,它到底是哪里来的,是哪个软件安装的。</li><li>你的朋友发送一个软件安装包给你,但是你不知道这个软件是干什么的,它将会安装什么功能,它从哪里来。</li><li>你记得你安装了一个软件,但是却忘了这个软件的版本,并且找不到关于这个软件的文档。</li></ul><p>这些场景不胜枚举,但是你可以用rpm -q帮助你。</p><h3>RPM查询</h3><p>当你了解了如何查询软件的信息后,很容易你就能敲出一个查询命令来查询你想要知道的信息。-q是一个最基本的选项,查询可分为对包的查询以及对特定信息的查询,下面看看针对包的查询:</p><h4>包查询</h4><p>首先你要知道你要查询的是哪一个或那些包。</p><h5>包名片</h5><p>包名片是标识一个包的唯一字符串,每个名片包含了三种信息:</p><ol><li>安装包的名字</li><li>安装包的版本</li><li>安装包的发行号</li></ol><p>当使用一个包的名片来查询包的信息时,必须有包名,你也可以加上版本号和发行号。第一个限制是,包名片的三种信息的每一个都必须完全给出,如果要写上版本号,就须把版本号写全,如果要写上发行号,就须把发行号写全。如果只给出了三种信息的其中一个或两个,那么RPM在找包的时候就会省略右边的其它部分。第二个限制是,如果你指定了发行号,那么必须也要指定版本号。让我们以几个例子来说明:</p><p>假设,你最近安装了一个新版本的C库,但是你不记得版本号了:</p><pre><code># rpm -q libc
libc-5.2.18-1
</code></pre><p>这样的查询方式中,rpm会从已安装的软件中找到匹配你给出的信息的软件,并会把整个包名片都打印出来。在上面的例子中,假设系统也安装有版本为5.2.17的C库,那它也会显示出来。</p><p>下面的例子中,我们将会把版本号也包括进去查询:</p><pre><code># rpm -q rpm-2.3
rpm-2.3-1
</code></pre><p>注意,RPM对包名是比较挑剔的,例如,下面这些查询就查不到C库:</p><pre><code># rpm -q LibC
package LibC is not installed
# rpm -q lib
package lib is not installed
# rpm -q "lib*"
package lib* is not installed
# rpm -q libc-5
package libc-5 is not installed
# rpm -q libc-5.2.1
package libc-5.2.1 is not installed
</code></pre><p>正如你能看到的,RPM对于包名是大小写敏感的,并且不接受没有写全的包名、版本号和发行号。而且它也不能使用通配符。但是从上面我们可以看到,给出包名片的一部分信息依然是能找到该包的,rpm -q libc-5.2.18和rpm -q libc-5.2.18-1都能正确地找到包libc-5.2.18-1。</p><p>仅仅根据包名片来查询,显得有点寒酸。毕竟有的时候,你需要知道一个包的名字后才能去查询它的信息。不过,还有其他方式可以指定特定的包...</p><h5>-a:查询所有已经安装的包</h5><p>使用-a选项能查询到在你系统上安装好的所有包:</p><pre><code># rpm -qa
ElectricFence-2.0.5-2
ImageMagick-3.7-2
…
tetex-xtexsh-0.3.3-8
lout-3.06-4
</code></pre><p>其实-a的输出可能会有很多,因此上面省略了很多包。你可以使用<strong>more</strong>或者<strong>grep</strong>重定向输出。</p><h5>-f <file>:查询有哪些包拥有文件<file></h5><p>多少次你坐在你的电脑前看着一个程序,然而并不知道它是干嘛用的。如果这个程序是使用RPM安装的包所安装的一部分程序,那么很容易用RPM来得到你想要的答案。只要使用-f选项。例如,你找到一个陌生的程序叫做/bin/ls(好吧,大多数人对ls不陌生),想要知道是哪个包安装了它吗?很简单:</p><pre><code># rpm -qf /bin/ls
fileutils-3.12-3
</code></pre><p>如果你指定的文件并不是使用安装包安装的:</p><pre><code># rpm -qf .cshrc
file /home/ed/.cshrc is not owned by any package
</code></pre><h6>小骗局</h6><p>上述中,如果你得到了"not owned by any package",其实并不代表文件不是一个安装包安装的:</p><pre><code># rpm -qf /usr/X11/bin/xterm
file /usr/X11/bin/xterm is not owned by any package
</code></pre><p>通过上面的消息,我们很容易认为xterm不是任何一个包所安装的。<br>但是,让我们去它的目录下看看:</p><pre><code># ls -lF /usr
…
lrwxrwxrwx 1 root root 5 May 13 12:46 X11 -> X11R6/
drwxrwxr-x 7 root root 1024 Mar 21 00:21 X11R6/
…
</code></pre><p>关键的地方就是这个<strong>X11 -> X11R6/</strong>,这是一个符号链接,但RPM不认账,它只认X11,而不管X11R6。</p><p>怎么办,有两种方法:</p><ol><li><p>不要使用符号链接来查询,这通常很难做到。不过可以通过namei命令来追踪链接的真实文件地址</p><pre><code># namei /usr/X11/bin/xterm
f: /usr/X11/bin/xterm
d /
d usr
l X11 -> X11R6
d X11R6
d bin
- xterm
</code></pre></li></ol><p>很显然,上面命令的输出结果中很容易看出X11到X11R6的符号链接,所以你可以使用真实的文件地址来获取信息:</p><pre><code> # rpm -qf /usr/X11R6/bin/xterm
XFree86-3.1.2-5
</code></pre><ol start="2"><li><p>直接切换到你所要查询文件的目录下,即使是个符号链接,也能带你到真实的路径下:</p><pre><code> # cd /usr/X11/bin
# rpm -qf xterm
XFree86-3.1.2-5
</code></pre></li></ol><p>当你遇到"not owned by any package" 时, 如果你心生怀疑,那么就试试上面两种方法吧。</p><h5>-p <file>:查询一个特定的包</h5><p>到目前为止,每个为RPM查询指定安装包的方法都侧重于那些已经被安装好的包。-p选项就是用来查询那些还没安装到你系统的中的包的。</p><p>如果你需要了解一个包中的信息,但这个包的名字已经被改变过了。虽然包的名字改变过了,但是包的内容还没有改变过。我们查询的信息来源主要是从包里来。这时我们可以通过这个选项来找到这个包中到底包含了哪些内容:</p><pre><code># rpm -qp foo.bar
rpm-2.3-1
</code></pre><p>只需要一个命令,RPM就能给你想要的答案。</p><p>-p选项也能使用URL来指定包。</p><p>-p选项还可以从标准输入中查询包的信息,例如:</p><pre><code># cat bother-3.5-1.i386.rpm | rpm -qp -
bother-3.5-1
</code></pre><p>把cat的输出管道定向到RPM,最后一个<strong>-</strong>告诉RPM从标准输入中读取。</p><h5>-g <group>: 查询属于某个组<group>的包的信息</h5><p>当包的创建者在创建包时,需要对包进行分类,以把功能相似的包分类到一起。RPM能够通过分组来查询包,例如,有一个分组名叫Base,这个分组的包都提供了比较底层的Linux功能,我们可以看看这个分组有哪些包组成:</p><pre><code># rpm -qg Base
setup-1.5-1
pamconfig-0.50-5
filesystem-1.2-1
crontabs-1.3-1
dev-2.3-1
etcskel-1.1-1
initscripts-2.73-1
mailcap-1.0-3
pam-0.50-17
passwd-0.50-2
redhat-release-4.0-1
rootfiles-1.3-1
termcap-9.12.6-5
</code></pre><p>不过要注意的是分组名是大小写敏感的。rpm -qg base将不会查询到任何信息。</p><h5>--whatprovides <x>: 查询具有<x>功能的包</h5><p>RPM对包之间的依赖提供了很多支持,一个包可能依赖于另一包所提供的功能。</p><p>--whatprovides选项就是用来做这种事的,选项后面跟上一个功能,RPM就会查询到具有该功能的所有包,例如:</p><pre><code># rpm -q --whatprovides module-info
kernel-2.0.18-5
</code></pre><p>在这里,只有kernel-2.0.18-5提供了module-info的功能。</p><h5>--whatrequires <x>: 查询出需要依赖于功能<x>的所有包</h5><p>--whatrequires选项与上面的--whatprovides选项在逻辑上是对立的,用这个选项能找出需要依赖于特定功能的所有包,下面是一个例子:</p><pre><code># rpm -q --whatrequires module-info
kernelcfg-0.3-2
</code></pre><p>可以看到唯一需要module-info功能的包是kernelcfg-0.3-2</p><h4>信息查询</h4><p>指定好包后,你可能需要指出你需要查找这个包的哪方面的信息,正如我们已经看到的,默认情况下,使用rpm -q只会返回包名片,但是包的信息可不止这些哦。接下来我们会查看所有我们能查找到的信息:</p><h5>-i 查找包的详细信息</h5><p>在rpm -q上加上-i选项将会给出包的详细信息 :</p><pre><code># rpm -qi rpm
Name : rpm Distribution: Red Hat Linux Vanderbilt
Version : 2.3 Vendor: Red Hat Software
Release : 1 Build Date: Tue Dec 24 09:07:59 1996
Install date: Thu Dec 26 23:01:51 1996 Build Host: porky.redhat.com
Group : Utilities/System Source RPM: rpm-2.3-1.src.rpm
Size : 631157
Summary : Red Hat Package Manager
Description :
RPM is a powerful package manager, which can be used to build, install,
query, verify, update, and uninstall individual software packages. A
package consists of an archive of files, and package information, including
name, version, and description.
</code></pre><p>上面各项信息的意义如下所示:</p><ul><li>Name -- 包名</li><li>Version-- 包的版本</li><li>Release -- 发行号</li><li>Install date -- 安装日期</li><li>Group -- 分组名</li><li>Size -- 包的大小,以byte为单位</li><li>Summary -- 简洁的描述</li><li>Description -- 详细的描述</li><li>Distribution -- 所属产品</li><li>Vendor -- 软件的作者</li><li>Build Date -- 安装包的构建时间</li><li>Build Host -- 构建时所在的系统类型</li><li>Source RPM -- 源码包</li></ul><h5>-l:查找包所安装的所有文件</h5><p>通过加上-l选项能查找出安装包安装的所有文件:</p><pre><code># rpm -ql adduser
/usr/sbin/adduser
</code></pre><p>由于adduser只安装了一个文件,所以只有一个文件被列出来。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
详解Redis SORT命令
https://segmentfault.com/a/1190000002806846
2015-05-29T17:43:13+08:00
2015-05-29T17:43:13+08:00
ytbean
https://segmentfault.com/u/ytbean
6
<p>原文链接:<a href="https://link.segmentfault.com/?enc=98FnkCXHofjrkQIRf3Hshw%3D%3D.3OBtQ%2FeIW9WFG%2F64x4U%2BkkXWbr1J5%2BVZQb6WJu8pIRxOLz8u5NFTNmyk4EFq17jF" rel="nofollow">《详解Redis SORT命令》http://www.ytbean.com/posts/redis-sort/</a></p><h2>基本使用</h2><p><strong>命令格式</strong>: SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] </p><p>默认情况下,排序是基于数字的,各个元素将会被转化成双精度浮点数来进行大小比较,这是<code>SORT</code>命令最简单的形式,也就是下面这种形式:</p><p><code>SORT mylist</code></p><p>如果<code>mylist</code>是一个包含了数字元素的列表,那么上面的命令将会返回升序排列的一个列表。如果想要降序排序,要使用<code>DESC</code>描述符,如下所示:</p><p><code>SORT mylist DESC</code></p><p>如果<code>mylist</code>包含的元素是string类型的,想要按字典顺序排列这个列表,那么就要用到<code>ALPHA</code>描述符,如下所示:</p><p><code>SORT mylist ALPHA</code></p><p>Redis是基于UTF-8编码来处理数据的, 要确保你先设置好了!LC_COLLATE环境变量。</p><p>对于返回的元素个数也是可以进行限制的,只需要使用<code>LIMIT</code>描述符。使用这个描述符,你需要提供偏移量参数,来指定需要跳过多少个元素,返回多少个元素。 下面这个例子将会返回一个已经排序好了的列表中的10个元素,从下标为0开始:</p><p><code>SORT mylist LIMIT 0 10</code></p><p>几乎全部的描述符都可以同时使用。例如下面这个例子所示,它将会返回前5个元素,以字典顺序降序排列:</p><p><code>SORT mylist LIMIT 0 5 ALPHA DESC</code></p><h2>通过外部key来排序</h2><p>有时候,你会想要用外部的key来作为权重去排列列表或集合中的元素,而不是使用列表或集合中本来就有的元素来排列。<br>下面以一个例子来解释:<br>假设现在有一张这样的表(顺便吐槽一下,为什么这个markdown编辑器不支持表格,非要写html代码吗),有商品id,商品价格,以及商品的重量。</p><p><img src="/img/remote/1460000041527286" alt="image-20220310164247898" title="image-20220310164247898"></p><p>首先将上述表格的数据导入到redis中(redis版本:2.8.6)</p><pre><code>127.0.0.1:6379> lpush gid 1
(integer) 1
127.0.0.1:6379> lpush gid 2
(integer) 2
127.0.0.1:6379> lpush gid 3
(integer) 3
127.0.0.1:6379> lpush gid 4
(integer) 4
127.0.0.1:6379> set price_1 20
OK
127.0.0.1:6379> set price_2 40
OK
127.0.0.1:6379> set price_3 30
OK
127.0.0.1:6379> set price_4 10
OK
127.0.0.1:6379> set weight_1 3
OK
127.0.0.1:6379> set weight_2 2
OK
127.0.0.1:6379> set weight_3 4
OK
127.0.0.1:6379> set weight_4 1
OK
</code></pre><p>默认情况下对gid排序,只会按gid的值来排序</p><pre><code>127.0.0.1:6379> sort gid
1) "1"
2) "2"
3) "3"
4) "4"</code></pre><p>但是通过BY描述符,可以指定gid按照别的key来排序。例如我想要按照商品的价格来排序:</p><pre><code>127.0.0.1:6379> sort gid by price_*
1) "4"
2) "1"
3) "3"
4) "2"</code></pre><p>可以看到,gid为4的商品价格最低,为10,gid为2的商品价格最高,为40。</p><p>在这里,price_*中的*是一个占位符,先会把gid的值取出,代入到*中,再去查找price_*的值。例如在本例中,price_*中的*会分别被1,2,3,4代替,然后去取price_1,price_2,price_3,price_4的值来进行排序。</p><h2>跳过排序(不进行排序)</h2><p>BY描述符也可以使用一个根本不存在的key来作为排序规则,则直接导致的结果就是不进行任何排序。 这看上去似乎没什么用,但是如果你想要取得外部的keys时,跳过排序就非常有用了,这也会减少性能损耗。</p><p>要理解不排序的好处,首先要先了解一下GET描述符。<br>使用GET描述符,可以根据排序结果取出外部键值。例如以下命令:</p><pre><code>127.0.0.1:6379> sort gid get price_*
1) "20"
2) "40"
3) "30"
4) "10"</code></pre><p>先对gid排序,然后再分别取出price_{gid}的值。</p><p>也可以使用多个GET,获取多个外部key的值,例如:</p><pre><code>127.0.0.1:6379> sort gid get price_* get weight_*
1) "20" # 这是price
2) "3" # 这是weight,以下类推
3) "40"
4) "2"
5) "30"
6) "4"
7) "10"
8) "1"</code></pre><p>get也可以使用#来获取被排序的key的值,例如:</p><pre><code>127.0.0.1:6379> sort gid get # get price_*
1) "1" #这里取出的就是gid
2) "20" #这里是price,以下类推
3) "2"
4) "40"
5) "3"
6) "30"
7) "4"
8) "10"</code></pre><p>通过结合不排序和GET,能够在不排序的情况下,一次取得多个key的值。例如:</p><pre><code>127.0.0.1:6379> sort gid by no-existed get # get price_* get weight_*
1) "4" #这是gid
2) "10" #这是price
3) "1" #这是weight,以下类推
4) "3"
5) "30"
6) "4"
7) "2"
8) "40"
9) "2"
10) "1"
11) "20"
12) "3"</code></pre><h2>将排序结果保存在Redis中</h2><p>默认情况下,排序结果会返回给客户端。使用STORE描述符,可以将结果存储在指定key上,如果Key已经存在,则覆盖。而不将排序结果返回给客户端。用法如下:</p><pre><code>SORT mylist BY weight_* STORE resultkey</code></pre><p>下面举一个例子:</p><pre><code>127.0.0.1:6379> rpush num 1 3 5
(integer) 3
127.0.0.1:6379> rpush num 2 4 6
(integer) 6
127.0.0.1:6379> lrange num 0 -1
1) "1"
2) "3"
3) "5"
4) "2"
5) "4"
6) "6"
127.0.0.1:6379> sort num store num_sorted
(integer) 6
127.0.0.1:6379> lrange num_sorted 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"</code></pre><h2>将哈希表作为GET或BY的参数</h2><p>可以将哈希表的字段作为GET或者BY的参数,用法如下:</p><pre><code>SORT mylist BY weight_*->fieldname GET object_*->fieldname</code></pre><p>对于前面所用到的例子:</p><p><img src="/img/remote/1460000041527287" alt="image-20220310164353720" title="image-20220310164353720"></p><p>我们可以不把price_{gid}和weight_{id}用string类型来保存,而是对于每一个gid,创建一个包含price和weight字段的hash表good_info_{gid}来存储商品信息。</p><pre><code>127.0.0.1:6379> hmset good_info_1 price 20 weight 3
OK
127.0.0.1:6379> hmset good_info_2 price 40 weight 2
OK
127.0.0.1:6379> hmset good_info_3 price 30 weight 4
OK
127.0.0.1:6379> hmset good_info_4 price 10 weight 1
OK</code></pre><p>之后, by 和 get 选项都可以用 key->field 的格式来获取哈希表中的域的值, 其中 key 表示哈希表键, 而 field 则表示哈希表的域:</p><pre><code>127.0.0.1:6379> sort gid by good_info_*->price
1) "4"
2) "1"
3) "3"
4) "2"
127.0.0.1:6379> sort gid by good_info_*->price get good_info_*->weight
1) "1"
2) "3"
3) "4"
4) "2"</code></pre><h2>返回值</h2><p>如果没有使用 store 描述符,返回列表形式的排序结果。<br>如果使用 store 参数,返回排序结果的元素数量。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Redis 主从 Replication 的配置
https://segmentfault.com/a/1190000002692598
2015-04-20T18:02:35+08:00
2015-04-20T18:02:35+08:00
ytbean
https://segmentfault.com/u/ytbean
4
<p>原文链接:<a href="https://link.segmentfault.com/?enc=b%2Bg6omzhYr%2F5d1twLXCN%2Bg%3D%3D.cStLqrwWrFDqXdOis5N40EjHyExyZotTSLPH%2B%2Bfnyzrfz5%2BGdPoL%2FbRJ1Tmo1gH1pGjPShkjA%2F3ROWApRES95g%3D%3D" rel="nofollow">《Redis 主从 Replication 的配置》http://www.ytbean.com/posts/redis-replication-config/</a></p><h2>概述</h2><p>Redis的replication机制允许slave从master那里通过网络传输拷贝到完整的数据备份。具有以下特点:</p><ul><li>异步复制。从2.8版本开始,slave能不时地从master那里获取到数据。</li><li>允许单个master配置多个slave</li><li>slave允许其它slave连接到自己。一个slave除了可以连接master外,它还可以连接其它的slave。形成一个图状的架构。</li><li>master在进行replication时是非阻塞的,这意味着在replication期间,master依然能够处理客户端的请求。</li><li>slave在replication期间也是非阻塞的,也可以接受来自客户端的请求,但是它用的是之前的旧数据。可以通过配置来决定slave是否在进行replication时用旧数据响应客户端的请求,如果配置为否,那么slave将会返回一个错误消息给客户端。不过当新的数据接收完全后,必须将新数据与旧数据替换,即删除旧数据,在替换数据的这个时间窗口内,slave将会拒绝客户端的请求和连接。</li><li>一般使用replication来可以实现扩展性,例如说,可以将多个slave配置为“只读”,或者是纯粹的数据冗余备份。</li><li>能够通过replication来避免master每次持久化时都将整个数据集持久化到硬盘中。只需把master配置为不进行<strong>save</strong>操作(把配置文件中<strong>save</strong>相关的配置项注释掉即可),然后连接上一个slave,这个slave则被配置为不时地进行<strong>save</strong>操作的。不过需要注意的是,在这个用例中,必须确保master不会自动启动。更多详情请继续往下读。</li></ul><h2>Master持久化功能关闭时Replication的安全性</h2><p>当有需要使用到replication机制时,一般都会强烈建议把master的持久化开关打开。即使为了避免持久化带来的延迟影响,不把持久化开关打开,那么也应该把master配置为不会自动启动的。</p><p>为了更好地理解当一个不进行持久化的master如果允许自动启动所带来的危险性。可以看看下面这种失败情形:</p><blockquote>假设我们有一个redis节点A,设置为master,并且关闭持久化功能,另外两个节点B和C是它的slave,并从A复制数据。<br>如果A节点崩溃了导致所有的数据都丢失了,它会有重启系统来重启进程。但是由于持久化功能被关闭了,所以即使它重启了,它的数据集是空的。<br>而B和C依然会通过replication机制从A复制数据,所以B和C会从A那里复制到一份空的数据集,并用这份空的数据集将自己本身的非空的数据集替换掉。于是就相当于丢失了所有的数据。</blockquote><p>即使使用一些HA工具,比如说sentinel来监控master-slaves集群,也会发生上述的情形,因为master可能崩溃后迅速恢复。速度太快而导致sentinel无法察觉到一个failure的发生。</p><p>当数据的安全很重要、持久化开关被关闭并且有replication发生的时候,那么应该禁止实例的自启动。</p><h2>replication工作原理</h2><p>如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个<code>SYNC</code>命令给master请求复制数据。</p><p>master收到<code>SYNC</code>命令后,会在后台进行数据持久化,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份数据集发送给slave,slave会把接收到的数据进行持久化,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。</p><p>当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。</p><p>当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,支持部分复制。</p><h2>数据部分复制</h2><p>从2.8版本开始,slave与master能够在网络连接断开重连后只进行部分数据复制。</p><p>master会在其内存中创建一个复制流的等待队列,master和它所有的slave都维护了复制的数据下标和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果进程id变化了,或者数据下标不可用,那么将会进行一次全部数据的复制。</p><p>支持部分数据复制的命令是<code>PSYNC</code></p><h2>不需硬盘参与的Replication</h2><p>一般情况下,一次复制需要将内存的数据写到硬盘中,再将数据从硬盘读进内存,再发送给slave。</p><p>对于速度比较慢的硬盘,这个操作会给master带来性能上的损失。Redis2.8版本开始,实验性地加上了无硬盘复制的功能。这个功能能将数据从内存中直接发送到slave,而不用经过硬盘的存储。</p><blockquote>不过这个功能目前处于实验阶段,还未正式发布。</blockquote><h2>相关配置</h2><p>与replication相关的配置比较简单,只需要把下面一行加到slave的配置文件中:</p><pre><code>slaveof 192.168.1.1 6379</code></pre><p>你只需要把ip地址和端口号改一下。当然,你也可以通过客户端发送<code>SLAVEOF</code>命令给slave。</p><p>部分数据复制有一些可调的配置参数,请参考redis.conf文件。</p><p>无硬盘复制功能可以通过<code>repl-diskless-sync</code>来配置,另外一个配置项<code>repl-diskless-sync-delay</code>用来配置当收到第一个请求时,等待多个slave一起来请求之间的间隔时间。</p><h2>只读的slave</h2><p>从redis2.6版本开始,slave支持只读模式,而且是默认的。可以通过配置项<code>slave-read-only</code>来进行配置,并且支持客户端使用<code>CONFIG SET</code>命令来动态修改配置。</p><p>只读的slave会拒绝所有的写请求,只读的slave并不是为了防范不可信的客户端,毕竟一些管理命令例如<code>DEBUG</code>和<code>CONFIG</code>在只读模式下还是可以使用的。如果确实要确保安全性,那么可以在配置文件中将一些命令重新命名。</p><p>也许你会感到很奇怪,为什么能够将一个只读模式的slave恢复为可写的呢,尽管可写,但是只要slave一同步master的数据,就会丢失那些写在slave的数据。不过还是有一些合法的应用场景需要存储瞬时数据会用到这个特性。不过,之后可能会考虑废除掉这个特性。</p><p>Setting a slave to authenticate to a master</p><p>如果master通过<code>requirepass</code>配置项设置了密码,slave每次同步操作都需要验证密码,可以通过在slave的配置文件中添加以下配置项:</p><pre><code>masterauth <password></code></pre><p>也可以通过客户端在运行时发送以下命令:</p><pre><code>config set masterauth <password></code></pre><h2>至少N个slave才允许向master写数据</h2><p>从redis2.8版本开始,master可以被配置为,只有当master当前有至少N个slave连接着的时候才接受写数据的请求。</p><p>然而,由于redis是异步复制的,所以它并不能保证slave会受到一个写请求,所以总有一个数据丢失的时间窗口存在。</p><p>这个机制的工作原理如下所示:</p><ul><li>slave每秒发送ping心跳给master,询问当前复制了多少数据。</li><li>master会记录下它上次收到某个slave的ping心跳是什么时候。</li><li>使用者可以配置一个时间,来指定ping心跳的发送不应超过的一个超时时间</li></ul><p>如果master有至少N个slave,并且ping心跳的超时不超过M秒,那么它就会接收写请求。</p><p>也许你会认为这情形好似<strong>CAP</strong>理论中弱化版的C(consistency),因为写请求并不能保证数据的一致性,但这样做,至少数据丢失被限制在了限定的时间内。即M秒。</p><p>如果N和M的条件都无法达到,那么master会回复一个错误信息。写请求也不会被处理。</p><p>有两个配置项用来配置上文中提到的N和M:</p><pre><code> min-slaves-to-write <number of slaves>
min-slaves-max-lag <number of seconds></code></pre><p>如果需要了解更多,请查阅redis.conf配置文件。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Jedis 源码分析
https://segmentfault.com/a/1190000002690506
2015-04-20T00:02:44+08:00
2015-04-20T00:02:44+08:00
ytbean
https://segmentfault.com/u/ytbean
13
<p>原文链接:<a href="https://link.segmentfault.com/?enc=3YacK9NogW5CUIuEBLaBYQ%3D%3D.%2FDdMiGVGvvPRzy90pwcGfRaveh7jqx4Xs%2F6OPiQaXyf%2FvqV9ACfuW5gQSgsOzm3B" rel="nofollow">《Jedis 源码分析》http://www.ytbean.com/posts/jedis-source-walk/</a></p><h2>概述</h2><p>Jedis是Redis官方推荐的Java客户端,更多Redis的客户端可以参考Redis官网<a href="https://link.segmentfault.com/?enc=3QVl4NbWnyLBshr3BlauZw%3D%3D.OnyBR9%2FMvS8WHki2II4n1qpf85iDexK7a6ReCU18GO8%3D" rel="nofollow">客户端列表</a>。</p><h2>JedisSentinelPool</h2><h3>简介</h3><p>Redis-Sentinel作为官方推荐的HA解决方案,Jedis也在客户端角度实现了对Sentinel的支持,主要实现在<code>JedisSentinelPool.java</code>这个类中,下文会分析这个类的实现。</p><h3>属性</h3><p>JedisSentinelPool类里有以下的属性:</p><pre><code class="java"> //基于apache的commom-pool2的对象池配置
protected GenericObjectPoolConfig poolConfig;
//超时时间,默认是2000
protected int timeout = Protocol.DEFAULT_TIMEOUT;
//sentinel的密码
protected String password;
//redis数据库的数目
protected int database = Protocol.DEFAULT_DATABASE;
//master监听器,当master的地址发生改变时,会触发这些监听者
protected Set<MasterListener> masterListeners = new HashSet<MasterListener>();
protected Logger log = Logger.getLogger(getClass().getName());
//Jedis实例创建工厂
private volatile JedisFactory factory;
//当前的master,HostAndPort是一个简单的包装了ip和port的模型类
private volatile HostAndPort currentHostMaster;
</code></pre><h3>构造器</h3><p>构造器的代码如下:</p><pre><code class="java">public JedisSentinelPool(String masterName, Set<String> sentinels, final GenericObjectPoolConfig poolConfig, int timeout, final String password, final int database) {
this.poolConfig = poolConfig;
this.timeout = timeout;
this.password = password;
this.database = database;
HostAndPort master = initSentinels(sentinels, masterName);
initPool(master);
}</code></pre><p>构造器一开始对实例变量进行赋值,参数sentinels是客户端所需要打交道的Redis-Sentinel,允许有多个,用一个集合来盛装。</p><p>然后通过initSentinels方法与sentinel沟通后,确定当前sentinel所监视的master是哪一个。然后通过master来创建好对象池,以便后续从对象池中取出一个Jedis实例,来对master进行操作。</p><h3>initSentinels方法</h3><p>initSentinels方法的代码如下所示,我加了一些注释:</p><pre><code class="java"> private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
HostAndPort master = null;
boolean sentinelAvailable = false;
log.info("Trying to find master from available Sentinels...");
// 有多个sentinels,遍历这些个sentinels
for (String sentinel : sentinels) {
// host:port表示的sentinel地址转化为一个HostAndPort对象。
final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
log.fine("Connecting to Sentinel " + hap);
Jedis jedis = null;
try {
// 连接到sentinel
jedis = new Jedis(hap.getHost(), hap.getPort());
// 根据masterName得到master的地址,返回一个list,host= list[0], port =
// list[1]
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
// connected to sentinel...
sentinelAvailable = true;
if (masterAddr == null || masterAddr.size() != 2) {
log.warning("Can not get master addr, master name: " + masterName
+ ". Sentinel: " + hap + ".");
continue;
}
master = toHostAndPort(masterAddr);
log.fine("Found Redis master at " + master);
// 如果在任何一个sentinel中找到了master,不再遍历sentinels
break;
} catch (JedisConnectionException e) {
log.warning("Cannot connect to sentinel running @ " + hap
+ ". Trying next one.");
} finally {
// 关闭与sentinel的连接
if (jedis != null) {
jedis.close();
}
}
}
// 到这里,如果master为null,则说明有两种情况,一种是所有的sentinels节点都down掉了,一种是master节点没有被存活的sentinels监控到
if (master == null) {
if (sentinelAvailable) {
// can connect to sentinel, but master name seems to not
// monitored
throw new JedisException("Can connect to sentinel, but " + masterName
+ " seems to be not monitored...");
} else {
throw new JedisConnectionException(
"All sentinels down, cannot determine where is " + masterName
+ " master is running...");
}
}
//如果走到这里,说明找到了master的地址
log.info("Redis master running at " + master + ", starting Sentinel listeners...");
//启动对每个sentinels的监听
for (String sentinel : sentinels) {
final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
MasterListener masterListener = new MasterListener(masterName, hap.getHost(),
hap.getPort());
masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
</code></pre><p>可以看到<code>initSentinels</code>方法的参数有一个masterName,就是我们所需要查找的master的名字。<br>一开始,遍历多个sentinels,一个一个连接到sentinel,去询问关于masterName的消息,可以看到是通过<code>jedis.sentinelGetMasterAddrByName()</code>方法去连接sentinel,并询问当前的master的地址。点进这个方法去看看,源代码是这样写的:</p><pre><code class="java">/**
* <pre>
* redis 127.0.0.1:26381> sentinel get-master-addr-by-name mymaster
* 1) "127.0.0.1"
* 2) "6379"
* </pre>
* @param masterName
* @return two elements list of strings : host and port.
*/
public List<String> sentinelGetMasterAddrByName(String masterName) {
client.sentinel(Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME, masterName);
final List<Object> reply = client.getObjectMultiBulkReply();
return BuilderFactory.STRING_LIST.build(reply);
}</code></pre><p>调用的是与Jedis绑定的client去发送一个<strong>"get-master-addr-by-name"</strong>命令。</p><p>回到<code>initSentinels</code>方法中,如果没有询问到master的地址,那就询问下一个sentinel。如果询问到了master的地址,那么将不再遍历sentinel集合,直接break退出循环遍历。</p><p>如果循环结束后,master的值为null,那么有两种可能:</p><ul><li>一种是所有的sentinel实例都不可用了</li><li>另外一种是,sentinel实例有可用的,但是没有监控名字为masterName的Redis。</li></ul><p>如果master为null,程序会抛出异常,不再往下走了。如果master不为null呢,继续往下走。</p><p>可以从代码中看到,为每个sentinel都启动了一个监听者<code>MasterListener</code>。MasterListener本身是一个线程,它会去订阅sentinel上关于master节点地址改变的消息。</p><p>接下来先分析构造方法中的另外一个方法:<code>initPool</code>。之后再看MasterListener的实现。</p><h3>initPool方法</h3><p>initPool的实现源代码如下所示:</p><pre><code class="java">private void initPool(HostAndPort master) {
if (!master.equals(currentHostMaster)) {
currentHostMaster = master;
if (factory == null) {
factory = new JedisFactory(master.getHost(), master.getPort(), timeout,
password, database);
initPool(poolConfig, factory);
} else {
factory.setHostAndPort(currentHostMaster);
// although we clear the pool, we still have to check the
// returned object
// in getResource, this call only clears idle instances, not
// borrowed instances
internalPool.clear();
}
log.info("Created JedisPool to master at " + master);
}
}
</code></pre><p>可以看到,作为参数传进来的master会与实例变量currentHostMaster作比较,看看是否是相同的,为什么要作这个比较呢,因为前文中提到的<code>MasterListener</code>会在发现master地址改变以后,去调用<code>initPool</code>方法。<br>如果是第一次调用<code>initPool</code>方法(构造函数中调用),那么会初始化Jedis实例创建工厂,如果不是第一次调用(<code>MasterListener</code>中调用),那么只对已经初始化的工厂进行重新设置。<br>从以上也可以看出为什么<code>currentHostMaster</code>和<code>factory</code>这两个变量为什么要声明为<code>volatile</code>,它们会在多线程环境下被访问和修改,因此必须保证<strong>可见性</strong>。<br>第一次调用时,会调用<code>initPool(poolConfig, factory)</code>方法。<br>看看这个方法的源代码:</p><pre><code class="java">public void initPool(final GenericObjectPoolConfig poolConfig,
PooledObjectFactory<T> factory) {
if (this.internalPool != null) {
try {
closeInternalPool();
} catch (Exception e) {
}
}
this.internalPool = new GenericObjectPool<T>(factory, poolConfig);
}</code></pre><p>基本上只干了一件事:初始化内部对象池。</p><h3>MasterListener监听者线程</h3><p>直接看它的run方法实现吧:</p><pre><code class="java">
public void run() {
running.set(true);
while (running.get()) {
j = new Jedis(host, port);
try {
//订阅sentinel上关于master地址改变的消息
j.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
log.fine("Sentinel " + host + ":" + port + " published: "
+ message + ".");
String[] switchMasterMsg = message.split(" ");
if (switchMasterMsg.length > 3) {
if (masterName.equals(switchMasterMsg[0])) {
initPool(toHostAndPort(Arrays.asList(
switchMasterMsg[3], switchMasterMsg[4])));
} else {
log.fine("Ignoring message on +switch-master for master name "
+ switchMasterMsg[0]
+ ", our master name is " + masterName);
}
} else {
log.severe("Invalid message received on Sentinel " + host
+ ":" + port + " on channel +switch-master: "
+ message);
}
}
}, "+switch-master");
} catch (JedisConnectionException e) {
if (running.get()) {
log.severe("Lost connection to Sentinel at " + host + ":" + port
+ ". Sleeping 5000ms and retrying.");
try {
Thread.sleep(subscribeRetryWaitTimeMillis);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
} else {
log.fine("Unsubscribing from Sentinel at " + host + ":" + port);
}
}
}
}
</code></pre><p>可以看到它依然委托了Jedis去与sentinel打交道,订阅了关于master地址变换的消息,当master地址变换时,就会再调用一次<code>initPool</code>方法,重新设置对象池相关的设置。</p><h3>尾声</h3><p>Jedis的JedisSentinelPool的实现仅仅适用于单个master-slave。<br>现在有了更多的需求,既需要sentinel提供的自动主备切换机制,又需要客户端能够做数据分片(Sharding),类似于memcached用一致性哈希进行数据分片。<br>接下来可能会自己在现有Jedis上实现一个支持一致性哈希分片的ShardedJedisSentinelPool。</p><h2>Sharded</h2><h3>概述</h3><p>当业务的数据量非常庞大时,需要考虑将数据存储到多个缓存节点上,如何定位数据应该存储的节点,一般用的是一致性哈希算法。Jedis在客户端角度实现了一致性哈希算法,对数据进行分片,存储到对应的不同的redis实例中。<br>Jedis对Sharded的实现主要是在<code>ShardedJedis.java</code>和<code>ShardedJedisPool.java</code>中。本文主要介绍ShardedJedis的实现,ShardedJedisPool是基于apache的common-pool2的对象池实现。</p><h3>继承关系</h3><p>ShardedJedis--->BinaryShardedJedis--->Sharded <Jedis, JedisShardInfo></p><h3>构造函数</h3><p>查看其构造函数</p><pre><code class="java">public ShardedJedis(List<JedisShardInfo> shards, Hashing algo, Pattern keyTagPattern) {
super(shards, algo, keyTagPattern);
}</code></pre><p>构造器参数解释:</p><ul><li>shards是一个JedisShardInfo的列表,一个JedisShardedInfo类代表一个数据分片的主体。</li><li>algo是用来进行数据分片的算法</li><li>keyTagPattern,自定义分片算法所依据的key的形式。例如,可以不针对整个key的字符串做哈希计算,而是类似对<strong>thisisa{key}</strong>中包含在大括号内的字符串进行哈希计算。</li></ul><p><strong><code>JedisShardInfo</code></strong>是什么样的?</p><pre><code class="java">public class JedisShardInfo extends ShardInfo<Jedis> {
public String toString() {
return host + ":" + port + "*" + getWeight();
}
private int connectionTimeout;
private int soTimeout;
private String host;
private int port;
private String password = null;
private String name = null;
// Default Redis DB
private int db = 0;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public JedisShardInfo(String host) {
super(Sharded.DEFAULT_WEIGHT);
URI uri = URI.create(host);
if (JedisURIHelper.isValid(uri)) {
this.host = uri.getHost();
this.port = uri.getPort();
this.password = JedisURIHelper.getPassword(uri);
this.db = JedisURIHelper.getDBIndex(uri);
} else {
this.host = host;
this.port = Protocol.DEFAULT_PORT;
}
}
public JedisShardInfo(String host, String name) {
this(host, Protocol.DEFAULT_PORT, name);
}
public JedisShardInfo(String host, int port) {
this(host, port, 2000);
}
public JedisShardInfo(String host, int port, String name) {
this(host, port, 2000, name);
}
public JedisShardInfo(String host, int port, int timeout) {
this(host, port, timeout, timeout, Sharded.DEFAULT_WEIGHT);
}
public JedisShardInfo(String host, int port, int timeout, String name) {
this(host, port, timeout, timeout, Sharded.DEFAULT_WEIGHT);
this.name = name;
}
public JedisShardInfo(String host, int port, int connectionTimeout, int soTimeout, int weight) {
super(weight);
this.host = host;
this.port = port;
this.connectionTimeout = connectionTimeout;
this.soTimeout = soTimeout;
}
public JedisShardInfo(String host, String name, int port, int timeout, int weight) {
super(weight);
this.host = host;
this.name = name;
this.port = port;
this.connectionTimeout = timeout;
this.soTimeout = timeout;
}
public JedisShardInfo(URI uri) {
super(Sharded.DEFAULT_WEIGHT);
if (!JedisURIHelper.isValid(uri)) {
throw new InvalidURIException(String.format(
"Cannot open Redis connection due invalid URI. %s", uri.toString()));
}
this.host = uri.getHost();
this.port = uri.getPort();
this.password = JedisURIHelper.getPassword(uri);
this.db = JedisURIHelper.getDBIndex(uri);
}
@Override
public Jedis createResource() {
return new Jedis(this);
}
/**
* 省略setters和getters
**/
}</code></pre><p>可见JedisShardInfo包含了一个redis节点ip地址,端口号,name,密码等等相关信息。要构造一个ShardedJedis,提供一个或多个JedisShardInfo。</p><p>最终构造函数的实现在其父类<code>Sharded</code>里面</p><pre><code class="java">public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {
this.algo = algo;
this.tagPattern = tagPattern;
initialize(shards);
}</code></pre><h3>哈希环的初始化</h3><p>Sharded类里面维护了一个TreeMap,基于红黑树实现,用来盛放经过一致性哈希计算后的redis节点,另外维护了一个LinkedHashMap,用来保存ShardInfo与Jedis实例的对应关系。<br><strong>定位的流程如下</strong><br>先在TreeMap中找到对应key所对应的ShardInfo,然后通过ShardInfo在LinkedHashMap中找到对应的Jedis实例。</p><p>Sharded类对这些实例变量的定义如下所示:</p><pre><code class="java">public static final int DEFAULT_WEIGHT = 1;
private TreeMap<Long, S> nodes;
private final Hashing algo;
private final Map<ShardInfo<R>, R> resources = new LinkedHashMap<ShardInfo<R>, R>();
/**
* The default pattern used for extracting a key tag. The pattern must have
* a group (between parenthesis), which delimits the tag to be hashed. A
* null pattern avoids applying the regular expression for each lookup,
* improving performance a little bit is key tags aren't being used.
*/
private Pattern tagPattern = null;
// the tag is anything between {}
public static final Pattern DEFAULT_KEY_TAG_PATTERN = Pattern.compile("\\{(.+?)\\}");</code></pre><p>接下来看其构造函数中的initialize方法</p><pre><code class="java">private void initialize(List<S> shards) {
nodes = new TreeMap<Long, S>();
for (int i = 0; i != shards.size(); ++i) {
final S shardInfo = shards.get(i);
if (shardInfo.getName() == null)
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
}
else
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(
this.algo.hash(shardInfo.getName() + "*"
+ shardInfo.getWeight() + n), shardInfo);
}
resources.put(shardInfo, shardInfo.createResource());
}
}</code></pre><p>可以看到,它对每一个ShardInfo通过一定规则计算其哈希值,然后存到TreeMap中,这里它实现了一致性哈希算法中虚拟节点的概念,因为我们可以看到同一个ShardInfo不止一次被放到TreeMap中,数量是,权重*160。<br>增加了虚拟节点的一致性哈希有很多好处,能避免数据在redis节点间分布不均匀。</p><p>然后,在LinkedHashMap中放入ShardInfo以及其对应的Jedis实例,通过调用其自身的createSource()来得到jedis实例。</p><h3>数据定位</h3><p>从ShardedJedis的代码中可以看到,无论进行什么操作,都要先根据key来找到对应的Redis,然后返回一个可供操作的Jedis实例。</p><p>例如其set方法:</p><pre><code class="java">public String set(String key, String value) {
Jedis j = getShard(key);
return j.set(key, value);
}</code></pre><p>而getShard方法则在Sharded.java中实现,其源代码如下所示:</p><pre><code class="java">public R getShard(byte[] key) {
return resources.get(getShardInfo(key));
}
public R getShard(String key) {
return resources.get(getShardInfo(key));
}
public S getShardInfo(byte[] key) {
SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
if (tail.isEmpty()) {
return nodes.get(nodes.firstKey());
}
return tail.get(tail.firstKey());
}
public S getShardInfo(String key) {
return getShardInfo(SafeEncoder.encode(getKeyTag(key)));
}
</code></pre><p>可以看到,先通过getShardInfo方法从TreeMap中获得对应的ShardInfo,然后根据这个ShardInfo就能够再LinkedHashMap中获得对应的Jedis实例了。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
图的理解及其 Java 实现
https://segmentfault.com/a/1190000002685939
2015-04-17T17:11:53+08:00
2015-04-17T17:11:53+08:00
ytbean
https://segmentfault.com/u/ytbean
11
<p>原文链接:<a href="https://link.segmentfault.com/?enc=uwP6BUuLf%2BMscMo8Ef%2FwJw%3D%3D.wjmYodR%2FzIwxo23NPaGgogXcRCsziHfxEp29TbiuN%2B5WLS6GHrm%2Ftob6AwTQPEp1" rel="nofollow">《图的理解及其 Java 实现》http://www.ytbean.com/posts/graph-in-java/</a></p><h2>图的基本概念</h2><p>图是什么,图是一种数据结构,一种非线性结构,所谓的非线性结构,浅显地理解的话,就是图的存储不是像链表这样的线性存储结构,而是由两个集合所组成的一种数据结构。</p><p>一个图中有两类东西,一种是结点,一种结点之间的连线。要用一种数据结构来表示的话,首先我们需要一个集合来存储所有的点,我们用V这个集合来表示(vertex),还需要另一个集合来存储所有的边,我们用E来表示(Edge),那么一个图就可以表示为:</p><blockquote><strong>G = (V,E)</strong></blockquote><p>有的图的边是有方向的,有的是没有方向的。</p><p>(A,B)表示A结点与B结点之间无方向的边,<A,B>则表示方向为从A到B的一条边,当然,如果是<B,A>,则方向相反。因此从边的方向我们就可以把图分为<strong>有向图</strong>和<strong>无向图</strong>两种。</p><p>一个图中的元素有很多,例如:</p><blockquote>完全图,邻接节点,结点的度,路径,权,路径长度,子图,连通图和强连通图,生成树,简单路径和回路...</blockquote><p>本文只谈到一些容易混淆的概念</p><h3>完全图,连通图,与强连通图</h3><p>完全图可分为有向完全图和无向完全图两种,如果一个图的任意两个结点之间有且只有一条边,则称此图为无向完全图,若任意两个结点之间有且只有方向相反的两条边,则称为有向完全图。</p><p>那么连通图与完全图有什么区别呢?连通图是指在无向图中,若图中任意一对结点之间都有路径可达,则称这个无向图是连通图,而强连通图则是对应于有向图来说的,其特点与连通图是一样的。只不过是有向的,所以加了"强"。</p><p>连通图与完全图的区别就是,完全图要求任意两点之间有边,而连通图则是要求有路径。边和路径是有区别的。</p><h3>邻接结点</h3><p>一个结点的邻接节点,对于无向图来说,就是与这个结点相连的结点,至少有一个。</p><p>对于有向图来说,由于边是有方向的,所以一个结点的邻接节点是指以这个结点为开头,所指向的那些结点。</p><h3>结点的度</h3><p>度是针对结点来说的, 又分为出度和入度,看到“出度入度”,我们不难想到这是与边和边的方向有关的。<br>对于无向图来说,没有出度入度之分,一个结点的度就是经过这个结点的边的数目(或者是与这个结点相关联的边的数目),对于有向图来说,出度就是指以这个结点为起始的边的条数(箭头向外),入度则是以这个点为终点的边的条数(箭头向内)。</p><p><strong>出 = 箭头向外,入 = 箭头向内</strong></p><h3>权</h3><p>权是指一条边所附带的数据信息,比如说一个结点到另一个结点的距离,或者花费的时间等等都可以用权来表示。带权的图也称为网格或网。</p><h3>子图</h3><p>跟一个集合有子集一样,图也有子图。可以类比理解。</p><h2>存储结构</h2><p>要存储一个图,我们知道图既有结点,又有边,对于有权图来说,每条边上还带有权值。常用的图的存储结构主要有以下二种:</p><ul><li>邻接矩阵</li><li>邻接表</li></ul><h2>邻接矩阵</h2><p>我们知道,要表示结点,我们可以用一个一维数组来表示,然而对于结点和结点之间的关系,则无法简单地用一维数组来表示了,我们可以用二维数组来表示,也就是一个矩阵形式的表示方法。</p><p>我们假设A是这个二维数组,那么A中的一个元素aij不仅体现出了结点vi和结点vj的关系,而且aij的值正可以表示权值的大小。</p><p>以下是一个<strong>无向图</strong>的邻接矩阵表示示例:</p><p><img src="/img/remote/1460000041525658" alt="无向图邻接矩阵" title="无向图邻接矩阵"></p><p>从上图我们可以看到,无向图的邻接矩阵是<strong>对称矩阵</strong>,也<strong>一定</strong>是对称矩阵。且其左上角到右下角的对角线上值为零(对角线上表示的是相同的结点)</p><p><strong>有向图</strong>的邻接矩阵是怎样的呢?</p><p><img src="/img/remote/1460000041525659" alt="有向图邻接矩阵" title="有向图邻接矩阵"></p><p>对于带权图,aij的值可用来表示权值的大小,上面两张图是不带权的图,因此它们值都是1。</p><h2>邻接表</h2><p>我们知道,图的邻接矩阵存储方法用的是一个n*n的矩阵,当这个矩阵是稠密的矩阵(比如说当图是完全图的时候),那么当然选择用邻接矩阵存储方法。<br>可是如果这个矩阵是一个稀疏的矩阵呢,这个时候邻接表存储结构就是一种更节省空间的存储结构了。<br>对于上文中的无向图,我们可以用邻接表来表示,如下:</p><p><img src="/img/remote/1460000041525660" alt="无向图邻接表" title="无向图邻接表"></p><p>每一个结点后面所接的结点都是它的邻接结点。</p><h2>邻接矩阵与邻接表的比较</h2><p>当图中结点数目较小且边较多时,采用邻接矩阵效率更高。<br>当节点数目远大且边的数目远小于相同结点的完全图的边数时,采用邻接表存储结构更有效率。</p><h2>邻接矩阵的Java实现</h2><h3>邻接矩阵模型类</h3><p>邻接矩阵模型类的类名为AMWGraph.java,能够通过该类构造一个邻接矩阵表示的图,且提供插入结点,插入边,取得某一结点的第一个邻接结点和下一个邻接结点。</p><pre><code class="java">
import java.util.ArrayList;
import java.util.LinkedList;
/**
* @description 邻接矩阵模型类
* @author beanlam
* @time 2015.4.17
*/
public class AMWGraph {
private ArrayList vertexList;//存储点的链表
private int[][] edges;//邻接矩阵,用来存储边
private int numOfEdges;//边的数目
public AMWGraph(int n) {
//初始化矩阵,一维数组,和边的数目
edges=new int[n][n];
vertexList=new ArrayList(n);
numOfEdges=0;
}
//得到结点的个数
public int getNumOfVertex() {
return vertexList.size();
}
//得到边的数目
public int getNumOfEdges() {
return numOfEdges;
}
//返回结点i的数据
public Object getValueByIndex(int i) {
return vertexList.get(i);
}
//返回v1,v2的权值
public int getWeight(int v1,int v2) {
return edges[v1][v2];
}
//插入结点
public void insertVertex(Object vertex) {
vertexList.add(vertexList.size(),vertex);
}
//插入结点
public void insertEdge(int v1,int v2,int weight) {
edges[v1][v2]=weight;
numOfEdges++;
}
//删除结点
public void deleteEdge(int v1,int v2) {
edges[v1][v2]=0;
numOfEdges--;
}
//得到第一个邻接结点的下标
public int getFirstNeighbor(int index) {
for(int j=0;j<vertexList.size();j++) {
if (edges[index][j]>0) {
return j;
}
}
return -1;
}
//根据前一个邻接结点的下标来取得下一个邻接结点
public int getNextNeighbor(int v1,int v2) {
for (int j=v2+1;j<vertexList.size();j++) {
if (edges[v1][j]>0) {
return j;
}
}
return -1;
}
}</code></pre><h3>邻接矩阵模型类的测试</h3><p>接下来根据下面一个有向图来设置测试该模型类</p><p><img src="/img/remote/1460000041525661" alt="邻接矩阵图" title="邻接矩阵图"></p><p>TestAMWGraph.java测试程序如下所示:</p><pre><code class="java">
/**
* @description AMWGraph类的测试类
* @author beanlam
* @time 2015.4.17
*/
public class TestAMWGraph {
public static void main(String args[]) {
int n=4,e=4;//分别代表结点个数和边的数目
String labels[]={"V1","V1","V3","V4"};//结点的标识
AMWGraph graph=new AMWGraph(n);
for(String label:labels) {
graph.insertVertex(label);//插入结点
}
//插入四条边
graph.insertEdge(0, 1, 2);
graph.insertEdge(0, 2, 5);
graph.insertEdge(2, 3, 8);
graph.insertEdge(3, 0, 7);
System.out.println("结点个数是:"+graph.getNumOfVertex());
System.out.println("边的个数是:"+graph.getNumOfEdges());
graph.deleteEdge(0, 1);//删除<V1,V2>边
System.out.println("删除<V1,V2>边后...");
System.out.println("结点个数是:"+graph.getNumOfVertex());
System.out.println("边的个数是:"+graph.getNumOfEdges());
}
}</code></pre><p>控制台输出结果如下图所示:</p><p><img src="/img/remote/1460000041525662" alt="image-20220310141417275" title="image-20220310141417275"></p><h2>遍历</h2><p>图的遍历,所谓遍历,即是对结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略:</p><ul><li>深度优先遍历</li><li>广度优先遍历</li></ul><h3>深度优先</h3><p>深度优先遍历,从初始访问结点出发,我们知道初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点。总结起来可以这样说:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。</p><p>我们从这里可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。</p><p>具体算法表述如下:</p><ol><li>访问初始结点v,并标记结点v为已访问。</li><li>查找结点v的第一个邻接结点w。</li><li>若w存在,则继续执行4,否则算法结束。</li><li>若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123)。</li><li>查找结点v的w邻接结点的下一个邻接结点,转到步骤3。</li></ol><p>例如下图,其深度优先遍历顺序为 <code>1->2->4->8->5->3->6->7</code></p><p><img src="/img/remote/1460000041525663" alt="image-20220310141552753" title="image-20220310141552753"></p><h3>广度优先</h3><p>类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。</p><p>具体算法表述如下:</p><ol><li>访问初始结点v并标记结点v为已访问。</li><li>结点v入队列</li><li>当队列非空时,继续执行,否则算法结束。</li><li>出队列,取得队头结点u。</li><li>查找结点u的第一个邻接结点w。</li><li>若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤:<br> 1). 若结点w尚未被访问,则访问结点w并标记为已访问。<br> 2). 结点w入队列<br> 3). 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。</li></ol><p>如下图,其广度优先算法的遍历顺序为:1->2->3->4->5->6->7->8</p><p><img src="/img/remote/1460000041525664" alt="image-20220310141624456" title="image-20220310141624456"></p><h3>Java 实现</h3><p>在原先类的基础上增加了两个遍历的函数,分别是 <code>depthFirstSearch()</code> 和 <code>broadFirstSearch()</code> 分别代表深度优先和广度优先遍历。</p><pre><code class="java">import java.util.ArrayList;
import java.util.LinkedList;
/**
* @description 邻接矩阵模型类
* @author beanlam
* @time 2015.4.17
*/
public class AMWGraph {
private ArrayList vertexList;//存储点的链表
private int[][] edges;//邻接矩阵,用来存储边
private int numOfEdges;//边的数目
public AMWGraph(int n) {
//初始化矩阵,一维数组,和边的数目
edges=new int[n][n];
vertexList=new ArrayList(n);
numOfEdges=0;
}
//得到结点的个数
public int getNumOfVertex() {
return vertexList.size();
}
//得到边的数目
public int getNumOfEdges() {
return numOfEdges;
}
//返回结点i的数据
public Object getValueByIndex(int i) {
return vertexList.get(i);
}
//返回v1,v2的权值
public int getWeight(int v1,int v2) {
return edges[v1][v2];
}
//插入结点
public void insertVertex(Object vertex) {
vertexList.add(vertexList.size(),vertex);
}
//插入结点
public void insertEdge(int v1,int v2,int weight) {
edges[v1][v2]=weight;
numOfEdges++;
}
//删除结点
public void deleteEdge(int v1,int v2) {
edges[v1][v2]=0;
numOfEdges--;
}
//得到第一个邻接结点的下标
public int getFirstNeighbor(int index) {
for(int j=0;j<vertexList.size();j++) {
if (edges[index][j]>0) {
return j;
}
}
return -1;
}
//根据前一个邻接结点的下标来取得下一个邻接结点
public int getNextNeighbor(int v1,int v2) {
for (int j=v2+1;j<vertexList.size();j++) {
if (edges[v1][j]>0) {
return j;
}
}
return -1;
}
//私有函数,深度优先遍历
private void depthFirstSearch(boolean[] isVisited,int i) {
//首先访问该结点,在控制台打印出来
System.out.print(getValueByIndex(i)+" ");
//置该结点为已访问
isVisited[i]=true;
int w=getFirstNeighbor(i);//
while (w!=-1) {
if (!isVisited[w]) {
depthFirstSearch(isVisited,w);
}
w=getNextNeighbor(i, w);
}
}
//对外公开函数,深度优先遍历,与其同名私有函数属于方法重载
public void depthFirstSearch() {
for(int i=0;i<getNumOfVertex();i++) {
//因为对于非连通图来说,并不是通过一个结点就一定可以遍历所有结点的。
if (!isVisited[i]) {
depthFirstSearch(isVisited,i);
}
}
}
//私有函数,广度优先遍历
private void broadFirstSearch(boolean[] isVisited,int i) {
int u,w;
LinkedList queue=new LinkedList();
//访问结点i
System.out.print(getValueByIndex(i)+" ");
isVisited[i]=true;
//结点入队列
queue.addlast(i);
while (!queue.isEmpty()) {
u=((Integer)queue.removeFirst()).intValue();
w=getFirstNeighbor(u);
while(w!=-1) {
if(!isVisited[w]) {
//访问该结点
System.out.print(getValueByIndex(w)+" ");
//标记已被访问
isVisited[w]=true;
//入队列
queue.addLast(w);
}
//寻找下一个邻接结点
w=getNextNeighbor(u, w);
}
}
}
//对外公开函数,广度优先遍历
public void broadFirstSearch() {
for(int i=0;i<getNumOfVertex();i++) {
if(!isVisited[i]) {
broadFirstSearch(isVisited, i);
}
}
}
}</code></pre><p>上面的public声明的depthFirstSearch()和broadFirstSearch()函数,是为了应对当该图是非连通图的情况,如果是非连通图,那么只通过一个结点是无法完全遍历所有结点的。</p><p>下面根据上面用来举例的图来构造测试类:</p><pre><code class="java">public class TestSearch {
public static void main(String args[]) {
int n=8,e=9;//分别代表结点个数和边的数目
String labels[]={"1","2","3","4","5","6","7","8"};//结点的标识
AMWGraph graph=new AMWGraph(n);
for(String label:labels) {
graph.insertVertex(label);//插入结点
}
//插入九条边
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.insertEdge(3, 7, 1);
graph.insertEdge(4, 7, 1);
graph.insertEdge(2, 5, 1);
graph.insertEdge(2, 6, 1);
graph.insertEdge(5, 6, 1);
graph.insertEdge(1, 0, 1);
graph.insertEdge(2, 0, 1);
graph.insertEdge(3, 1, 1);
graph.insertEdge(4, 1, 1);
graph.insertEdge(7, 3, 1);
graph.insertEdge(7, 4, 1);
graph.insertEdge(6, 2, 1);
graph.insertEdge(5, 2, 1);
graph.insertEdge(6, 5, 1);
System.out.println("深度优先搜索序列为:");
graph.depthFirstSearch();
System.out.println();
System.out.println("广度优先搜索序列为:");
graph.broadFirstSearch();
}
}</code></pre><p>运行后控制台输出如下:</p><p><img src="/img/remote/1460000041525665" alt="image-20220310141659906" title="image-20220310141659906"></p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>
Redis Sentinel机制与用法
https://segmentfault.com/a/1190000002680804
2015-04-16T17:13:17+08:00
2015-04-16T17:13:17+08:00
ytbean
https://segmentfault.com/u/ytbean
75
<p>原文链接:<a href="https://link.segmentfault.com/?enc=6YtE10SS2k%2FC0q9R%2BC424Q%3D%3D.IBQ%2BNnbqpTivcGQXnLgtFy0sFdU6tn78S1Cmv2uwcDb8Q7pua0Y1R8VbvwG%2FFiqK%2BZgB8PxXH8Z3%2B7filiz6kQ%3D%3D" rel="nofollow">《Redis Sentinel 机制与用法》http://www.ytbean.com/posts/redis-sentinel-intro/</a></p><h2>概述</h2><p>Redis-Sentinel 是 Redis 官方推荐的高可用性(HA)解决方案,当用 Redis 做 Master-slave 的高可用方案时,假如 master 宕机了,Redis 本身(包括它的很多客户端)都没有实现自动进行主备切换,而 Redis-sentine l本身也是一个独立运行的进程,它能监控多个 master-slave集群,发现 master 宕机后能进行自动切换。</p><p>它的主要功能有以下几点:</p><ul><li>不时地监控redis是否按照预期良好地运行;</li><li>如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);</li><li>能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。</li></ul><h2>Sentinel 支持集群</h2><p>很显然,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后(sentinel本身也有单点问题,single-point-of-failure)整个集群系统将无法按照预期的方式运行。所以有必要将sentinel集群,这样有几个好处:</p><ul><li>即使有一些sentinel进程宕掉了,依然可以进行redis集群的主备切换;</li><li>如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题);</li><li>如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。</li></ul><h2>Sentinel 版本</h2><p>Sentinel当前最新的稳定版本称为<strong>Sentinel 2</strong>(与之前的<strong>Sentinel 1</strong>区分开来)。随着redis2.8的安装包一起发行。安装完Redis2.8后,可以在<strong>redis2.8/src/</strong>里面找到Redis-sentinel的启动程序。</p><blockquote><strong>强烈建议</strong>:<br>如果你使用的是redis2.6(sentinel版本为<strong>sentinel 1</strong>),你最好应该使用redis2.8版本的<strong>sentinel 2</strong>,因为sentinel 1有很多的Bug,已经被官方弃用,所以强烈建议使用redis2.8以及sentinel 2。</blockquote><h2>运行 Sentinel</h2><p>运行sentinel有两种方式:</p><ul><li><p>第一种</p><pre><code class="shell">redis-sentinel /path/to/sentinel.conf</code></pre></li><li><p>第二种</p><pre><code class="shell">redis-server /path/to/sentinel.conf --sentinel</code></pre></li></ul><p>以上两种方式,都必须指定一个sentinel的配置文件sentinel.conf,如果不指定,将无法启动sentinel。sentinel默认监听26379端口,所以运行前必须确定该端口没有被别的进程占用。</p><h2>Sentinel的配置</h2><p>Redis源码包中包含了一个sentinel.conf文件作为sentinel的配置文件,配置文件自带了关于各个配置项的解释。典型的配置项如下所示:</p><pre><code class="shell"> sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5</code></pre><p>上面的配置项配置了两个名字分别为mymaster和resque的master,配置文件只需要配置master的信息就好啦,不用配置slave的信息,因为slave能够被自动检测到(master节点会有关于slave的消息)。需要注意的是,配置文件在sentinel运行期间是会被动态修改的,例如当发生主备切换时候,配置文件中的master会被修改为另外一个slave。这样,之后sentinel如果重启时,就可以根据这个配置来恢复其之前所监控的redis集群的状态。</p><p><strong>接下来我们将一行一行地解释上面的配置项</strong>:</p><p><code>sentinel monitor mymaster 127.0.0.1 6379 2</code></p><p>这一行代表sentinel监控的master的名字叫做<strong>mymaster</strong>,地址为<strong>127.0.0.1:6379</strong>,行尾最后的一个<strong>2</strong>代表什么意思呢?我们知道,网络是不可靠的,有时候一个sentinel会因为网络堵塞而误以为一个master redis已经死掉了,当sentinel集群式,解决这个问题的方法就变得很简单,只需要多个sentinel互相沟通来确认某个master是否真的死了,这个<strong>2</strong>代表,当集群中有2个sentinel认为master死了时,才能真正认为该master已经不可用了。(sentinel集群中各个sentinel也有互相通信,通过gossip协议)。</p><p>除了第一行配置,我们发现剩下的配置都有一个统一的格式:</p><p><code>sentinel <option_name> <master_name> <option_value></code></p><p>接下来我们根据上面格式中的<strong>option_name</strong>一个一个来解释这些配置项:</p><ul><li><p><strong>down-after-milliseconds</strong><br>sentinel会向master发送心跳<strong><em>PING</em></strong>来确认master是否存活,如果master在<strong>“一定时间范围”</strong>内不回应<strong><em>PONG</em></strong> 或者是回复了一个错误消息,那么这个sentinel会<strong>主观地</strong>(单方面地)认为这个master已经不可用了(subjectively down, 也简称为SDOWN)。而这个down-after-milliseconds就是用来指定这个<strong>“一定时间范围”</strong>的,单位是毫秒。</p><blockquote>不过需要注意的是,这个时候sentinel并不会马上进行failover主备切换,这个sentinel还需要参考sentinel集群中其他sentinel的意见,如果超过某个数量的sentinel也<strong>主观地</strong>认为该master死了,那么这个master就会被<strong>客观地</strong>(注意哦,这次不是主观,是客观,与刚才的subjectively down相对,这次是objectively down,简称为ODOWN)认为已经死了。需要一起做出决定的sentinel数量在上一条配置中进行配置。</blockquote></li><li><strong>parallel-syncs</strong><br>在发生failover主备切换时,这个选项指定了最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave处于不能处理命令请求的状态。</li></ul><p>其他配置项在sentinel.conf中都有很详细的解释。<br>所有的配置都可以在运行时用命令<code>SENTINEL SET command</code>动态修改。</p><h2>Sentinel的“仲裁会”</h2><p>前面我们谈到,当一个master被sentinel集群监控时,需要为它指定一个参数,这个参数指定了当需要判决master为不可用,并且进行failover时,所需要的sentinel数量,本文中我们暂时称这个参数为<strong>票数</strong></p><p>不过,当failover主备切换真正被触发后,failover并不会马上进行,还需要sentinel中的<strong>大多数</strong>sentinel授权后才可以进行failover。<br>当ODOWN时,failover被触发。failover一旦被触发,尝试去进行failover的sentinel会去获得“大多数”sentinel的授权(如果票数比大多数还要大的时候,则询问更多的sentinel)</p><p>这个区别看起来很微妙,但是很容易理解和使用。例如,集群中有5个sentinel,票数被设置为2,当2个sentinel认为一个master已经不可用了以后,将会触发failover,但是,进行failover的那个sentinel必须先获得至少3个sentinel的授权才可以实行failover。</p><p>如果票数被设置为5,要达到ODOWN状态,必须所有5个sentinel都主观认为master为不可用,要进行failover,那么得获得所有5个sentinel的授权。</p><h2>配置版本号</h2><p>为什么要先获得<strong>大多数</strong>sentinel的认可时才能真正去执行failover呢?</p><p>当一个sentinel被授权后,它将会获得宕掉的master的一份最新配置版本号,当failover执行结束以后,这个版本号将会被用于最新的配置。因为<strong>大多数</strong>sentinel都已经知道该版本号已经被要执行failover的sentinel拿走了,所以其他的sentinel都不能再去使用这个版本号。这意味着,每次failover都会附带有一个独一无二的版本号。我们将会看到这样做的重要性。</p><p>而且,sentinel集群都遵守一个规则:如果sentinel A推荐sentinel B去执行failover,A会等待一段时间后,自行再次去对同一个master执行failover,这个等待的时间是通过<code>failover-timeout</code>配置项去配置的。从这个规则可以看出,sentinel集群中的sentinel不会再同一时刻并发去failover同一个master,第一个进行failover的sentinel如果失败了,另外一个将会在一定时间内进行重新进行failover,以此类推。</p><p>redis sentinel保证了活跃性:如果<strong>大多数</strong>sentinel能够互相通信,最终将会有一个被授权去进行failover.<br>redis sentinel也保证了安全性:每个试图去failover同一个master的sentinel都会得到一个独一无二的版本号。</p><h2>配置传播</h2><p>一旦一个sentinel成功地对一个master进行了failover,它将会把关于master的最新配置通过广播形式通知其它sentinel,其它的sentinel则更新对应master的配置。</p><p>一个faiover要想被成功实行,sentinel必须能够向选为master的slave发送<code>SLAVE OF NO ONE</code>命令,然后能够通过<code>INFO</code>命令看到新master的配置信息。</p><p>当将一个slave选举为master并发送<code>SLAVE OF NO ONE</code>`后,即使其它的slave还没针对新master重新配置自己,failover也被认为是成功了的,然后所有sentinels将会发布新的配置信息。</p><p>新配在集群中相互传播的方式,就是为什么我们需要当一个sentinel进行failover时必须被授权一个版本号的原因。</p><p>每个sentinel使用##发布/订阅##的方式持续地传播master的配置版本信息,配置传播的##发布/订阅##管道是:<code>__sentinel__:hello</code>。</p><p>因为每一个配置都有一个版本号,所以以版本号最大的那个为标准。</p><p>举个栗子:假设有一个名为mymaster的地址为192.168.1.50:6379。一开始,集群中所有的sentinel都知道这个地址,于是为mymaster的配置打上版本号1。一段时候后mymaster死了,有一个sentinel被授权用版本号2对其进行failover。如果failover成功了,假设地址改为了192.168.1.50:9000,此时配置的版本号为2,进行failover的sentinel会将新配置广播给其他的sentinel,由于其他sentinel维护的版本号为1,发现新配置的版本号为2时,版本号变大了,说明配置更新了,于是就会采用最新的版本号为2的配置。</p><p>这意味着sentinel集群保证了第二种活跃性:一个能够互相通信的sentinel集群最终会采用版本号最高且相同的配置。</p><h2>SDOWN和ODOWN的更多细节</h2><p>sentinel对于<strong>不可用</strong>有两种不同的看法,一个叫<strong>主观不可用</strong>(SDOWN),另外一个叫<strong>客观不可用</strong>(ODOWN)。SDOWN是sentinel自己主观上检测到的关于master的状态,ODOWN需要一定数量的sentinel达成一致意见才能认为一个master客观上已经宕掉,各个sentinel之间通过命令<code>SENTINEL is_master_down_by_addr</code>来获得其它sentinel对master的检测结果。</p><p>从sentinel的角度来看,如果发送了<strong>*PING*</strong>心跳后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件。这个时间在配置中通过<code>is-master-down-after-milliseconds</code>参数配置。</p><p>当sentinel发送<strong>*PING*</strong>后,以下回复之一都被认为是合法的:</p><pre><code class="autoit">PING replied with +PONG.
PING replied with -LOADING error.
PING replied with -MASTERDOWN error.</code></pre><p>其它任何回复(或者根本没有回复)都是不合法的。</p><p>从SDOWN切换到ODOWN不需要任何一致性算法,只需要一个gossip协议:如果一个sentinel收到了足够多的sentinel发来消息告诉它某个master已经down掉了,SDOWN状态就会变成ODOWN状态。如果之后master可用了,这个状态就会相应地被清理掉。</p><p>正如之前已经解释过了,真正进行failover需要一个授权的过程,但是所有的failover都开始于一个ODOWN状态。</p><p>ODOWN状态只适用于master,对于不是master的redis节点sentinel之间不需要任何协商,slaves和sentinel不会有ODOWN状态。</p><h2>Sentinel之间和Slaves之间的自动发现机制</h2><p>虽然sentinel集群中各个sentinel都互相连接彼此来检查对方的可用性以及互相发送消息。但是你不用在任何一个sentinel配置任何其它的sentinel的节点。因为sentinel利用了master的<strong>发布/订阅</strong>机制去自动发现其它也监控了统一master的sentinel节点。</p><p>通过向名为<code>__sentinel__:hello</code>的管道中发送消息来实现。</p><p>同样,你也不需要在sentinel中配置某个master的所有slave的地址,sentinel会通过询问master来得到这些slave的地址的。</p><p>每个sentinel通过向每个master和slave的<strong>发布/订阅</strong>频道<code>__sentinel__:hello</code>每秒发送一次消息,来宣布它的存在。<br>每个sentinel也订阅了每个master和slave的频道<code>__sentinel__:hello</code>的内容,来发现未知的sentinel,当检测到了新的sentinel,则将其加入到自身维护的master监控列表中。<br>每个sentinel发送的消息中也包含了其当前维护的最新的master配置。如果某个sentinel发现<br>自己的配置版本低于接收到的配置版本,则会用新的配置更新自己的master配置。</p><p>在为一个master添加一个新的sentinel前,sentinel总是检查是否已经有sentinel与新的sentinel的进程号或者是地址是一样的。如果是那样,这个sentinel将会被删除,而把新的sentinel添加上去。</p><h2>网络隔离时的一致性</h2><p>redis sentinel集群的配置的一致性模型为最终一致性,集群中每个sentinel最终都会采用最高版本的配置。然而,在实际的应用环境中,有三个不同的角色会与sentinel打交道:</p><ul><li>Redis实例.</li><li>Sentinel实例.</li><li>客户端.</li></ul><p>为了考察整个系统的行为我们必须同时考虑到这三个角色。</p><p>下面有个简单的例子,有三个主机,每个主机分别运行一个redis和一个sentinel:</p><pre><code> +-------------+
| Sentinel 1 | <--- Client A
| Redis 1 (M) |
+-------------+
|
|
+-------------+ | +------------+
| Sentinel 2 |-----+-- / partition / ----| Sentinel 3 | <--- Client B
| Redis 2 (S) | | Redis 3 (M)|
+-------------+ +------------+</code></pre><p>在这个系统中,初始状态下redis3是master, redis1和redis2是slave。之后redis3所在的主机网络不可用了,sentinel1和sentinel2启动了failover并把redis1选举为master。</p><p>Sentinel集群的特性保证了sentinel1和sentinel2得到了关于master的最新配置。但是sentinel3依然持着的是就的配置,因为它与外界隔离了。</p><p>当网络恢复以后,我们知道sentinel3将会更新它的配置。但是,如果客户端所连接的master被网络隔离,会发生什么呢?</p><p>客户端将依然可以向redis3写数据,但是当网络恢复后,redis3就会变成redis的一个slave,那么,在网络隔离期间,客户端向redis3写的数据将会丢失。</p><p>也许你不会希望这个场景发生:</p><ul><li>如果你把redis当做缓存来使用,那么你也许能容忍这部分数据的丢失。</li><li>但如果你把redis当做一个存储系统来使用,你也许就无法容忍这部分数据的丢失了。</li></ul><p>因为redis采用的是异步复制,在这样的场景下,没有办法避免数据的丢失。然而,你可以通过以下配置来配置redis3和redis1,使得数据不会丢失。</p><pre><code>min-slaves-to-write 1
min-slaves-max-lag 10</code></pre><p>通过上面的配置,当一个redis是master时,如果它不能向至少一个slave写数据(<strong>上面的min-slaves-to-write</strong>指定了slave的数量),它将会拒绝接受客户端的写请求。由于复制是异步的,master无法向slave写数据意味着slave要么断开连接了,要么不在指定时间内向master发送同步数据的请求了(上面的<strong>min-slaves-max-lag</strong>指定了这个时间)。</p><h2>Sentinel状态持久化</h2><p>snetinel的状态会被持久化地写入sentinel的配置文件中。每次当收到一个新的配置时,或者新创建一个配置时,配置会被持久化到硬盘中,并带上配置的版本戳。这意味着,可以安全的停止和重启sentinel进程。</p><h2>无failover时的配置纠正</h2><p>即使当前没有failover正在进行,sentinel依然会使用当前配置去设置监控的master。特别是:</p><ul><li>根据最新配置确认为slaves的节点却声称自己是master(参考上文例子中被网络隔离后的的redis3),这时它们会被重新配置为当前master的slave。</li><li>如果slaves连接了一个错误的master,将会被改正过来,连接到正确的master。</li></ul><h2>Slave选举与优先级</h2><p>当一个sentinel准备好了要进行failover,并且收到了其他sentinel的授权,那么就需要选举出一个合适的slave来做为新的master。</p><p>slave的选举主要会评估slave的以下几个方面:</p><ul><li>与master断开连接的次数</li><li>Slave的优先级</li><li>数据复制的下标(用来评估slave当前拥有多少master的数据)</li><li>进程ID</li></ul><p>如果一个slave与master失去联系超过10次,并且每次都超过了配置的最大失联时间(<code>down-after-milliseconds option</code>),并且,如果sentinel在进行failover时发现slave失联,那么这个slave就会被sentinel认为不适合用来做新master的。</p><p>更严格的定义是,如果一个slave持续断开连接的时间超过</p><pre><code>(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state</code></pre><p>就会被认为失去选举资格。<br>符合上述条件的slave才会被列入master候选人列表,并根据以下顺序来进行排序:</p><ol><li>sentinel首先会根据slaves的优先级来进行排序,优先级越小排名越靠前(?)。</li><li>如果优先级相同,则查看复制的下标,哪个从master接收的复制数据多,哪个就靠前。</li><li>如果优先级和下标都相同,就选择进程ID较小的那个。</li></ol><p>一个redis无论是master还是slave,都必须在配置中指定一个slave优先级。要注意到master也是有可能通过failover变成slave的。</p><p>如果一个redis的slave优先级配置为0,那么它将永远不会被选为master。但是它依然会从master哪里复制数据。</p><h2>Sentinel和Redis身份验证</h2><p>当一个master配置为需要密码才能连接时,客户端和slave在连接时都需要提供密码。</p><p>master通过<code>requirepass</code>设置自身的密码,不提供密码无法连接到这个master。<br>slave通过<code>masterauth</code>来设置访问master时的密码。</p><p>但是当使用了sentinel时,由于一个master可能会变成一个slave,一个slave也可能会变成master,所以需要同时设置上述两个配置项。</p><h2>Sentinel API</h2><p>Sentinel默认运行在26379端口上,sentinel支持redis协议,所以可以使用redis-cli客户端或者其他可用的客户端来与sentinel通信。</p><p>有两种方式能够与sentinel通信:</p><ul><li>一种是直接使用客户端向它发消息</li><li>另外一种是使用<strong>发布/订阅</strong>模式来订阅sentinel事件,比如说failover,或者某个redis实例运行出错,等等。</li></ul><h3>Sentinel命令</h3><p>sentinel支持的合法命令如下:</p><ul><li><code>PING</code> sentinel回复<code>PONG</code>.</li><li><code>SENTINEL masters</code> 显示被监控的所有master以及它们的状态.</li><li><code>SENTINEL master <master name></code> 显示指定master的信息和状态;</li><li><code>SENTINEL slaves <master name></code> 显示指定master的所有slave以及它们的状态;</li><li><code>SENTINEL get-master-addr-by-name <master name></code> 返回指定master的ip和端口,如果正在进行failover或者failover已经完成,将会显示被提升为master的slave的ip和端口。</li><li><code>SENTINEL reset <pattern></code> 重置名字匹配该正则表达式的所有的master的状态信息,清楚其之前的状态信息,以及slaves信息。</li><li><code>SENTINEL failover <master name></code> 强制sentinel执行failover,并且不需要得到其他sentinel的同意。但是failover后会将最新的配置发送给其他sentinel。</li></ul><h3>动态修改Sentinel配置</h3><p>从redis2.8.4开始,sentinel提供了一组API用来添加,删除,修改master的配置。</p><blockquote>需要注意的是,如果你通过API修改了一个sentinel的配置,sentinel不会把修改的配置告诉其他sentinel。你需要自己手动地对多个sentinel发送修改配置的命令。</blockquote><p>以下是一些修改sentinel配置的命令:</p><ul><li><code>SENTINEL MONITOR <name> <ip> <port> <quorum> </code> 这个命令告诉sentinel去监听一个新的master</li><li><code>SENTINEL REMOVE <name></code> 命令sentinel放弃对某个master的监听</li><li><code>SENTINEL SET <name> <option> <value></code> 这个命令很像Redis的<code>CONFIG SET</code>命令,用来改变指定master的配置。支持多个<option><value>。例如以下实例:</li><li><code>SENTINEL SET objects-cache-master down-after-milliseconds 1000</code></li></ul><p>只要是配置文件中存在的配置项,都可以用<code>SENTINEL SET</code>命令来设置。这个还可以用来设置master的属性,比如说quorum(票数),而不需要先删除master,再重新添加master。例如:</p><pre><code>SENTINEL SET objects-cache-master quorum 5</code></pre><h3>增加或删除Sentinel</h3><p>由于有sentinel自动发现机制,所以添加一个sentinel到你的集群中非常容易,你所需要做的只是监控到某个Master上,然后新添加的sentinel就能获得其他sentinel的信息以及masterd所有的slave。</p><p>如果你需要添加多个sentinel,建议你一个接着一个添加,这样可以预防网络隔离带来的问题。你可以每个30秒添加一个sentinel。最后你可以用<code>SENTINEL MASTER mastername</code>来检查一下是否所有的sentinel都已经监控到了master。</p><p>删除一个sentinel显得有点复杂:因为sentinel永远不会删除一个已经存在过的sentinel,即使它已经与组织失去联系很久了。<br>要想删除一个sentinel,应该遵循如下步骤:</p><ol><li>停止所要删除的sentinel</li><li>发送一个<code>SENTINEL RESET * </code>命令给所有其它的sentinel实例,如果你想要重置指定master上面的sentinel,只需要把*号改为特定的名字,注意,需要一个接一个发,每次发送的间隔不低于30秒。</li><li>检查一下所有的sentinels是否都有一致的当前sentinel数。使用<code>SENTINEL MASTER mastername</code> 来查询。</li></ol><h3>删除旧master或者不可达slave</h3><p>sentinel永远会记录好一个Master的slaves,即使slave已经与组织失联好久了。这是很有用的,因为sentinel集群必须有能力把一个恢复可用的slave进行重新配置。</p><p>并且,failover后,失效的master将会被标记为新master的一个slave,这样的话,当它变得可用时,就会从新master上复制数据。</p><p>然后,有时候你想要永久地删除掉一个slave(有可能它曾经是个master),你只需要发送一个<code>SENTINEL RESET master</code>命令给所有的sentinels,它们将会更新列表里能够正确地复制master数据的slave。</p><h3>发布/订阅</h3><p>客户端可以向一个sentinel发送订阅某个频道的事件的命令,当有特定的事件发生时,sentinel会通知所有订阅的客户端。需要注意的是客户端只能订阅,不能发布。</p><p>订阅频道的名字与事件的名字一致。例如,频道名为<strong>sdown</strong> 将会发布所有与SDOWN相关的消息给订阅者。</p><p>如果想要订阅所有消息,只需简单地使用<code>PSUBSCRIBE * </code></p><p>以下是所有你可以收到的消息的消息格式,如果你订阅了所有消息的话。第一个单词是频道的名字,其它是数据的格式。</p><p>注意:以下的instance details的格式是:</p><p><instance-type> <name> <ip> <port> @ <master-name> <master-ip> <master-port></p><p>如果这个redis实例是一个master,那么@之后的消息就不会显示。</p><pre><code> +reset-master <instance details> -- 当master被重置时.
+slave <instance details> -- 当检测到一个slave并添加进slave列表时.
+failover-state-reconf-slaves <instance details> -- Failover状态变为reconf-slaves状态时
+failover-detected <instance details> -- 当failover发生时
+slave-reconf-sent <instance details> -- sentinel发送SLAVEOF命令把它重新配置时
+slave-reconf-inprog <instance details> -- slave被重新配置为另外一个master的slave,但数据复制还未发生时。
+slave-reconf-done <instance details> -- slave被重新配置为另外一个master的slave并且数据复制已经与master同步时。
-dup-sentinel <instance details> -- 删除指定master上的冗余sentinel时 (当一个sentinel重新启动时,可能会发生这个事件).
+sentinel <instance details> -- 当master增加了一个sentinel时。
+sdown <instance details> -- 进入SDOWN状态时;
-sdown <instance details> -- 离开SDOWN状态时。
+odown <instance details> -- 进入ODOWN状态时。
-odown <instance details> -- 离开ODOWN状态时。
+new-epoch <instance details> -- 当前配置版本被更新时。
+try-failover <instance details> -- 达到failover条件,正等待其他sentinel的选举。
+elected-leader <instance details> -- 被选举为去执行failover的时候。
+failover-state-select-slave <instance details> -- 开始要选择一个slave当选新master时。
no-good-slave <instance details> -- 没有合适的slave来担当新master
selected-slave <instance details> -- 找到了一个适合的slave来担当新master
failover-state-send-slaveof-noone <instance details> -- 当把选择为新master的slave的身份进行切换的时候。
failover-end-for-timeout <instance details> -- failover由于超时而失败时。
failover-end <instance details> -- failover成功完成时。
switch-master <master name> <oldip> <oldport> <newip> <newport> -- 当master的地址发生变化时。通常这是客户端最感兴趣的消息了。
+tilt -- 进入Tilt模式。
-tilt -- 退出Tilt模式。</code></pre><h3>TILT 模式</h3><p>redis sentinel非常依赖系统时间,例如它会使用系统时间来判断一个PING回复用了多久的时间。<br>然而,假如系统时间被修改了,或者是系统十分繁忙,或者是进程堵塞了,sentinel可能会出现运行不正常的情况。<br>当系统的稳定性下降时,TILT模式是sentinel可以进入的一种的保护模式。当进入TILT模式时,sentinel会继续监控工作,但是它不会有任何其他动作,它也不会去回应<code>is-master-down-by-addr</code>这样的命令了,因为它在TILT模式下,检测失效节点的能力已经变得让人不可信任了。<br>如果系统恢复正常,持续30秒钟,sentinel就会退出TITL模式。</p><h3>-BUSY状态</h3><blockquote>注意:该功能还未实现。</blockquote><p>当一个脚本的运行时间超过配置的运行时间时,sentinel会返回一个-BUSY 错误信号。如果这件事发生在触发一个failover之前,sentinel将会发送一个SCRIPT KILL命令,如果script是只读的话,就能成功执行。</p><p><img src="/img/remote/1460000041525246" alt="联系我" title="联系我"></p>