小无相功(上): Java基础大全-合辑
本系列文章会有多篇,是大魏的读书和实验笔记,参考的书籍如下。大魏大致按照从上到下的阅读顺序,但书写blog的时候会把几本书的内容串起来,从实战的角度进行书写。(否则就失去了写的意义)。有读者对这这些书感兴趣,可以从京东购买。
类的实例化与反射
一、调用类的方式
方式1:直接创建类的实例化
如下图红框所示,直接创建Ticket1的实例ticket。
补充说明一下这段代码。
Java中现成的创建主要有两种方式:
(1)继承java.lang包下的Thread类。这个类里有两个方法:start()方法、run()方法。但由于一类只能能有一个父类。所以这种用的相对少。
(2)Thread提供另外构造方法Thread(Runable target),Runnable是一个接口,只有一个run()方法。这个接口这种方式使用的多一些。
在上图中:
Ticket1类实现了Runnable接口。将名为Ticket1的class进行实例化,实例名为ticket。
通过Thread类的构造方法将实例ticket作为参数传入,并指定线程名(如ticket1),同时调用Thread类的start()方法。
Start()方法会去调用ticket对象中的run()方法。
方式2:将类实例化与线程实例化合并
第二种模式与第一种无本质区别。将EmergencyThread类的实例作为参数注入到Thread的构造方法。然后在下一步调用Thread中的start方法。
方式3:实例化时转成父类
YieldThread继承了Thread类。在YieldThread类中,定义了一个有参构造方法,并且构造方法中,用到了super()。super可以理解为是指向自己父类对象的一个指针。所以,当创建线程实例时,可以使用YieldThread()直接将参数传递到父类Thread。
第四种方式:静态方法调用
下图红框位置,就是调用了静态方法getArea()。
在实际使用中,更多的是使用实例化赋值。通过new实例化对象后,会生产对象实例,占用内存空间。
二、类传参的三种方式:
利用有参构建方法:在类实例化时传入
利用普通方法:需要实例化类,通过”类.方法”方式传入。
利用静态方法:不需要实例化,通过“类.方法”方式传入。
三.反射
反射是在JavaBean处引入的。JavaBean本身就是Java类,但需要满足以下两个规范:
必须包含一个公用的无参构造方法(编译器可以自动生成)。
提供公共的setter和getter方法,以便外部程序设置和获取JavaBean的属性。
反射是Java中的一个重要的概念。既然有个“反”字,那自然是针对“正”来说。
如上文所示,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。们可以理解为“正”。
而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。这时候,我们使用 JDK 提供的反射 API 进行反射调用。
我们看下面的例子,通过x.getClass().getName方法可以得到完整的“包.类”名。
getClass()方法是从顶级类Object集成来的。
Class类表示一个类的本身,通过Class可以完整的得到一个类的结构,包括类中的方法定义。Class类的常用方法包括:
获取Class类实例的方法有:
(1)通过“对象.getClass()”方式获取该对象的Class实例
(2)通过Class类的静态方法forName(),用类的全路径名获取一个Class实例。这种是最常用的!
(3)通过“类名.class”的方式获取Class实例。
四、通过反射创建对象。
通过反射创建对象有两种方式:
(1)使用无参构造方法
(2)使用有参构造方法
我们查看使用第一种方式。
下图第一个红框定义了包名、class名。
第二个红框中,实例化Person时,并未向构造方法传递参数(参数的传入是调用setter设置的)。
通过有参构造放肆实例化对象,需要通过Class类的getConstructors()获取本类的全部构造方法、向构造方法中传递一个对象数组、通过Constructors类实例化对象。
我们再看使用有参构造方法的示例,如下图红框所示,在实例化Person对象时,传入了参数。
五、通过反射访问属性
反射不仅能创建对象,还能访问属性。反射式访问属性是通过Filed类实现的。这个类提供set()和get()方法用于设置和获取属性。如果是私有属性,需要在set()和get()方法使用前,使用Field类中的setAccessible()将需要操作的属性设置成恶意被外界访问。
六、通过反射调用方法
当我们获取类对应的Class对象后,通过Class对象的getMethods()方法和getMethod()方法获取全部的方法或某一个方法。这两个方法的返回值,分别是Method对象和Method对象数组。每个Method对象都对应一个方法(invoke)。
在上图中,第一条红线是获取到SayHello方法的实例:md。然后,用md这个方法再创建新的实例,创建的时候,注入参数。
从应用角度看Socket
Socket这个概念,如果从网络角度解释,往往需要很多描述才能说清楚,而且对于初学者来说,云里雾里。但大魏发现如果从应用角度,就好理解的多了。
我们知道现在应用最广泛的通讯协议是TCP/IP协议。TCP/IP包含:TCP、UDP、ICMP等协议组。我们先看一下TCP/IP的网络模型:
我们看到传输层有TCP和UDP两种协议,前者面向无连接,后者面向连接(三次握手)。这个大家都很清楚,接下来我们从Java角度看一下UDP和TCP的用法。
一、UDP通信实现
如果说的形象点,UDP的通讯过程就像货运公司在两个码头间发送货物。发送货物时,需要集装箱进行打包。Java JDK中提供了DatagramPacket类,这个类的实例对象就相当于一个集装箱,用于封装UDP中发送或接收的数据。
DatagramPacket类针对于UDP的发送端和接收端有不同的构造方法。四个构造方法(两类:发送端、接收端)介绍如下:
DatagramPacket数据包的作用相当于集装箱,但这还不够,还需要定义码头。JAVA SDK中DatagramSocket类的作用相当于码头,使用这个类的实例对象就可以发送和接受DatagramPacket数据包,如下图所示:
DatagramSocket的三个构造方法如下:
所以,从上面的描述中,我们能比较直接地感触到:Socket定义了UDP的发送端和接收端的通信基础(端口号等),接下来我们看通过Java写的UDP的发送端和接收端的Class。
需求:
1、使用UDP协议编写一个网络程序,设置接收端程序监听端口为8001,发送端发送的数据是“helloworld”。
提示:
1)使用new DatagramSocket(8001)构造方法创建接收端的DatagramSocket对象,调用receive()方法接收数据。
3)发送端使用send()方法发送数据。
4)使用close()方法释放Socket资源。
发送端:
importjava.net.*;
publicclass Test03 {
public static void main(String[] args)throws Exception {
DatagramSocket ds = new DatagramSocket(3000);
String str = "hello world";
DatagramPacket dp = newDatagramPacket(str.getBytes(), str.length(),
InetAddress.getByName("localhost"),8001);
ds.send(dp);
ds.close();
}
}
接受端:
importjava.net.*;
publicclass Test02 {
public static void main(String[] args)throws Exception {
byte[] buf = new byte[1024];
DatagramSocket ds = newDatagramSocket(8001);
DatagramPacket dp = newDatagramPacket(buf, 1024);
ds.receive(dp);
String str = new String(dp.getData(),0,dp.getLength());
System.out.println(str);
ds.close();
}
}
通过以上内容,相信读者对Socket有了一些感性的认识。
接下来我们看TCP的通信实现。
二、TCP通信实现
前文我们知道,UDP想通信,需要在通信的两端都创建Scoket对象;但UDP只有发送端和接收端。而TCO严格区分客户端(Socket)和服务器端(ServerSocket)。如下图所示:
服务器端(ServerSocket)的四种构造方法如下:
客户端(Socket)的三种构造方法:
接下来,我我们看一个需求和代码实现。
需求:
1、使用TCP协议编写一个网络程序,设置服务器程序监听端口为8002,当与客户端建立连接后,向客户端发送“helloworld”,客户端接负责将信息输出。
提示:
1)使用ServerSocket创建服务器端对象,监听8002端口,调用accept()方法等待客户端连接,当与客户端连接后,调用Socket的getOutputStream()方法获得输出流对象,输出“helloworld。
3)在服务器端和客户端都调用close()方法释放socket资源。
代码实现。
客户端:
importjava.io.*;
importjava.net.*;
publicclass Test05 {
public static void main(String[] args)throws Exception {
new TCPClient().connect();
}
}
classTCPClient {
public void connect() throws Exception {
Socket client = newSocket(InetAddress.getLocalHost(), 8002);
InputStream is =client.getInputStream();
byte[] buf = new byte[1024];
int len = is.read(buf);
System.out.println(new String(buf, 0,len));
client.close();
}
}
服务器端:
importjava.io.*;
importjava.net.*;
publicclass Test04 {
public static void main(String[] args)throws Exception {
new TCPServer().listen();
}
}
classTCPServer {
public void listen() throws Exception {
ServerSocket serverSocket = newServerSocket(8002);
Socket client = serverSocket.accept();
OutputStream os =client.getOutputStream();
os.write("hello world").getBytes());
Thread.sleep(5000);
os.close();
client.close();
}
}
通过上面代码,我们能够直接体会到Socket在TCP通信中的作用。TCP/UDP通讯的两端需要先对Socket进行实例化(相当于定义了两个码头),然后才能通信发送网络包。
HTTP及会话技术解析:大魏Java记4
一、关于HTTP的协议版本
HTTP的全称是Hyper Text Transfer Protocol的缩写,即超级文本传输协议。HTTP协议用于定义浏览器与服务器之间交换数据的过程以及数据本身的格式。
HTTP是无状态协议,对于事务处理没有记录能力。因此后续处理如果需要前面的信息,则它必须重传,这会导致每次连接传送的数据量增大.
HTTP目前主要有三个版本:1.0、1.1、2.0。
http1.0的交互过程如下图所示:
缺陷:浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接(TCP连接的新建成本很高,因为需要客户端和服务器三次握手),服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求;
解决方案:
添加头信息——非标准的Connection字段Connection: keep-alive
http1.1:
改进点:
持久连接
引入了持久连接,即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive(对于同一个域名,大多数浏览器允许同时建立6个持久连接)
管道机制
即在同一个TCP连接里面,客户端可以同时发送多个请求。
分块传输编码
即服务端没产生一块数据,就发送一块,采用”流模式”而取代”缓存模式”。
新增请求方式
PUT:请求服务器存储一个资源;
DELETE:请求服务器删除标识的资源;
OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项和需求;
TRACE:请求服务器回送收到的请求信息,主要用于测试或诊断;
CONNECT:保留将来使用
缺点:
虽然允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个请求,才会接着处理下一个请求。如果前面的处理特别慢,后面就会有许多请求排队等着。这将导致“队头堵塞”
避免方式:一是减少请求数,二是同时多开持久连接
HTTP/2.0
HTTP 2.0协议是在1.x基础上的升级而不是重写,1.x协议的方法,状态及api在2.0协议里是一样的。2.0协议重点是对终端用户的感知延迟、网络及服务器资源的使用等性能的优化。
特点:
采用二进制格式而非文本格式;
完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行;
使用报头压缩,降低开销
服务器推送
1. 二进制协议
HTTP/1.1 版的头信息肯定是文本(ASCII编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为”帧”:头信息帧和数据帧。
二进制协议解析起来更高效、“线上”更紧凑,更重要的是错误更少。
http2.0之所以能够突破http1.X标准的性能限制,改进传输性能,实现低延迟和高吞吐量,就是因为其新增了二进制分帧层。
在二进制分帧层上,http2.0会将所有传输信息分割为更小的消息和帧,并对它们采用二进制格式的编码将其封装,新增的二进制分帧层同时也能够保证http的各种动词,方法,首部都不受影响,兼容上一代http标准。其中,http1.X中的首部信息header封装到Headers帧中,而request body将被封装到Data帧中。如下图所示:
2. 完全多路复用
HTTP/2 复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了”队头堵塞”。
3. 报头压缩
HTTP 协议是没有状态,导致每次请求都必须附上所有信息。所以,请求的很多头字段都是重复的,比如Cookie,一样的内容每次请求都必须附带,这会浪费很多带宽,也影响速度。
对于相同的头部,不必再通过请求发送,只需发送一次;
HTTP/2 对这一点做了优化,引入了头信息压缩机制;
一方面,头信息使用gzip或compress压缩后再发送;
另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,产生一个索引号,之后就不发送同样字段了,只需发送索引号。
4. 服务器推送
HTTP/2 允许服务器未经请求,主动向客户端发送资源;
通过推送那些服务器任务客户端将会需要的内容到客户端的缓存中,避免往返的延迟。
二、HTTP的请求方式
HTTP主要有8种请求方式,如下所示:
在这8种HTTP请求方式中,GET和POST是最常用的。网页上的form表单的默认提交方式是GET(在form表单的method属性不设置时)。
在实际的开发中,我们通常都会使用POST方式发送请求,而不是GET,原因有两个:
(1)POST传输数据大小无限制
三、HTTP的请求消息与响应消息
HTTP的请求消息和响应消息是相对应的,都包含三大部分:
HTTP消息除了请求头和响应头外,还有一些通用头字段,如下所示:
三、会话技术
在Web开发中,服务器跟踪用户信息的技术成为会话技术。
会话技术有两种实现:
(1)Cookie:将会话的过程数据保存到用户浏览器上。
(2)Session: 将会话数据保存到服务器端。
Cookie技术
Cookie通过将会话过程的数据保存到用户的浏览器上,使浏览器和服务器可以更好地进行数据交互。
我们可以形象的将Cookie理解成我们在商场办的会员卡。卡上记录了个人信息、消费额度和积分额度等。以后每次去商场,商场根据会员就能很快了解到顾客的会员信息。
在Cookie模式下,当用户通过浏览器访问Web服务器时,web服务器会给客户发送一些信息,这些信息都保存在Cookie中。这样,当浏览器再次访问服务器时,都会在请求头中将Cookie发送给服务器,Web服务器端可以分辨出当前请求是由哪个用户发出的,方便服务器对浏览器做出正确的响应,如下图所示:
为了封装Cookie信息,前端开发语言必要要有对应的方式。传统Servlet API会通过javax.servlet.http.Cookie类来实现。里面包含生成Cookie信息和提取Cookie信息的方法。由于现在Servlet已经不再被大量使用,因此我们不展开说明。
JavaScript Cookie的详细内容,可以看这个链接:
https://github.com/js-cookie/js-cookie
Session技术
Cookie的技术可以将用户的信息保存在各自的浏览器中,并且可以在多次请求下实现数据的共享。但如果传递的信息比较多,使用Cookie技术会增大服务器端的程序处理难度。这时,可以使用Session实现,它是一种将会话数据保存到服务器上的技术。
如果形象点说,Session技术就好比医院发给病人的就医卡。就医卡上只有卡号,没有其他信息,病人去看病时,主要出示就医卡,医院就可以根据卡号查到病历档案的过程。
当浏览器访问Web服务器时,Servlet容器(还以此举例)会创建一个Session对象和ID属性。Session对象相当于病例档案,ID就相当于就医卡号。客户端后续访问服务器时,只要将标识号传递给服务器,服务器就能根据请求是哪个客户发的,从而选择与之对应的Session对象为其服务。
需要注意的是:由于客户端要接受、记录和发送Session对象的ID,而Session是借助Cookie技术来传递ID属性的。
Session保存用户信息如下图所示:
为了方便理解,我们通过两端代码分别验证Cookie和Session的功能。
需求1:设计一个类,使用Cookie技术实现显示用户上次访问时间的功能。
package cn.itcast.chapter06.cookie.example;
import java.io.IOException;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LastAccessServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
/*
* 设定一个 cookie 的name : lastAccessTime
* 读取客户端发送cookie 获得用户上次的访问时间显示*/
String lastAccessTime = null;
// 获取所有的cookie,并将这些cookie存放在数组中
Cookie[] cookies = request.getCookies();
for (int i = 0; cookies != null && i < cookies.length; i++) {
if ("lastAccess".equals(cookies[i].getName())) {
// 如果cookie的名称为lastAccess,则获取该cookie的值
lastAccessTime = cookies[i].getValue();
break;
}
}
// 判断是否存在名称为lastAccess的cookie
if (lastAccessTime == null) {
response.getWriter().print("您是首次访问本站!!!");
} else {
response.getWriter().print("您上次的访问时间"+lastAccessTime);
}
// 创建cookie,将当前时间作为cookie的值发送给客户端
Cookie cookie = new Cookie("lastAccess",new Date().toLocaleString());
cookie.setMaxAge(60*60);//保存1小时
//访问chapter06下资源时回送cookie
cookie.setPath("/chapter06");
// 发送 cookie
response.addCookie(cookie);
}
}
需求2::设计一个类,使用Session技术实现购物车功能。
public class PurchaseServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 获得用户购买的商品
String id = req.getParameter("id");
if (id == null) {
// 如果id为null,重定向到ListBookServlet页面
String url = "/chapter06/ListBookServlet";
resp.sendRedirect(url);
return;
}
Book book = BookDB.getBook(id);
// 创建或者获得用户的Session对象
HttpSession session = req.getSession();
// 从Session对象中获得用户的购物车
List<Book> cart = (List)
session.getAttribute("cart");
if (cart == null) {
// 首次购买,为用户创建一个购物车(List集合模拟购物车)
cart = new ArrayList<Book>();
// 将购物城存入Session对象
session.setAttribute("cart", cart);
}
// 将商品放入购物车
cart.add(book);
// 创建Cookie存放Session的标识号
Cookie cookie = new Cookie("JSESSIONID", session.getId());
cookie.setMaxAge(60 * 30);
cookie.setPath("/chapter06");
resp.addCookie(cookie);
// 重定向到购物车页面
String url = "/chapter06/CartServlet";
resp.sendRedirect(url);
}
}
事务处理与一个银行转账业务模型分析
银行转账业务是一个典型的事务,我们先介绍基本概念,这样后面的代码看起来就比较容易理解了。
一、事务处理
“事务处理”这个名字的概念,相信大多数朋友都有所了解。我们看一下维基百科中关于“事务处理”的定义:
“在计算机科学中,事务是无法被分割的操作,事务处理就是被分割为个体的信息处理。事务必须作为一个完整的单元成功或失败,不可能存在部分完成的事务。”
看着是不是有点绕?
好,我们换个视角看“事务处理”,从数据库视角。
在数据库操作中,事务是由一条或者多条SQL语句组成的一个工作单元。只有当事中的所有操作都正常完成,整个事务才被提交到数据库,如果意向操作没有完成,则整个事务被撤销。
例如zhangsan给lisi转200元钱,对应的SQL是;
UPDATE ACCOUNT set MONEY=MONEY-200 WHERE NAME='zhangsan'
UPDATE ACCOUNT set MONEY=MONEY+200 WHERE NAME='lisi';
转钱这件事,就是一个事务处理。所以,事务是以数据为核心的,而数据是存在关系型数据库上的。因此,应用如何操作数据库就成为事务处理中的关键。
Java应用操作数据库的模块是JDBC。
二、JDBC
JBDC的全称为:Java Database Connectivity,是一套用于执行SQL语句的Java API。应用程序可以通过这套API连接到关系型数据库,并使用SQL语句来完成对数据库的查询、更新、删除工作。
我们先看一下JDBC API的内容。
在下面的三色框中,中间部分是JDBC常用的API。
第一部分是DataSource接口的实现;
第三名部分是DBUtils工具库;
JDBC的实现细节如下图所示,包含三大部分:
(1)JDBC驱动管理器:负责注册特定的JDBC驱动器,通过java.sql.DeviceManager类实现。
(2)Java驱动器API;其中最主要的接口是:java.sql.Driver接口。
(3)JDBC驱动器:一种数据库驱动,由数据库厂商创建。JDBC启动器实现了JDBC驱动器API,负责与特定的数据库链接,以及处理通信细节。
一个完整的JDBC分为六大步骤:
通过DriverManager加载并注册DB驱动
通过DeiverManager获取数据库连接(DB connection)
通过Connection对象获取Statement对象。
使用Statement执行SQL语句,生成结果集ResultSet。
操作结果集,取出结果。
释放数据库资源
需要注意的是:由于Driver类的源码中,已经在静态代码块中完成了数据库驱动的注册,所有为了避免数据库驱动被重复注册,主要在程序中加载驱动类即可。
下面我们查看一个完成的JDBC示例。下图中的标号,就对应上文的6个步骤。我们看到第六步释放了Statement和Connection资源。
三、JDBC批处理
在实际开发中,经常需要向数据库发送多条SQL语句。这时如果逐条执行这些SQL语句,效率会很低。为此,JDBC提供了批处理机制,可以同时执行多条SQL语句。Statement和PreparedStatement都实现了批处理。
PreparedStatement是Statement的一个子类。PreparedStatement对象可以对SQL语句进行预编译。当相同的SQL语句再次执行时,数据库只需使用缓冲区的数据,而不需要对SQL语句再次编译,从而有效提高了数据的访问效率。
我们查看Statement加载和执行批处理的代码,如下图红框所示:
我们再看通过PreparedStatement加载和执行批处理的代码:
很明显,PreparedStatement的方式更灵活。
需要注意的是:Statement和PreparedStatement的executeBactch()方法的范围值都是int[]类型的,所以能够进行批处理的SQL语句必须是INSERT、UPDATE、DELETE等返回值为int类型的SQK语句。
四、JDBC如何处理事务
针对JDBC处理事务的操作,在Connection接口中,提供了三个相关的方法,具体如下:
1. setAutoCommit(boolean autoCommit):设置是否自动提交事务。
2. commit(): 提交事务
3.rollback(): 撤销事务。
接下来,我们通过一个案例展示J如何通过JDBC进行事务处理。
首先看一个工具类:JDBCUtils。
这个工具类避免我们每次操作都要书写:加载数据库驱动、检查数据连接以及关闭数据的连接的代码:
接下来,新建一个类,用于在两个账号之间转账。
从下图可以看出,本类使用了工具类JDBCUtils中的方法访问数据库。
JDBC关闭事务自动提交、提交事务、回滚事务的位置,我用红框标出来了,比较好理解。
五、集中式事务与分布式事务的本质
事务(Transaction )的核心是操作数据。事务必须要满足ACID的标准。而集中式架构,是很容易保证事务的ACID的。ACID中比较难实现的是C,实时一致性。集中式事务实现的方式就是传统应用架构如JavaEE+关系型数据库。我们也管这种事务叫本地事务。这种事务可以实现强一致性。
分布式架构,或者说分布式事务。分布式事务比较难以实现实时一致性,因此ACID不再适用。业内Bianc便提出了CAP和BASE等理论。所以说,集中式和分布式事务的根本区别是在于维护数据的强一致还是最终一致。
CAP 理论指出:无法设计一种分布式协议,使得同时完全具备CAP 三个属性,即1)该种协议 下的副本始终是强一致性,2)服务始终是可用的,3)协议可以容忍任何网络分区异常;分布式系统 协议只能在CAP 这三者间所有折中。
CAP 理论的定义很简单,CAP 三个字母分别代表了分布式系统中三个相互矛盾的属性:
Consistency (一致性):CAP 理论中的副本一致性特指强一致性;
Availiablity(可用性):指系统在出现异常时已经可以提供服务;
Tolerance to the partition of network (分区容忍):指系统可以对网络分区。这种异常情 况进行容错处理;
CAP 理论 的意义就在于明确提出了不要去妄图设计一种对CAP 三大属性都完全拥有的完美系统,因为这种系 统在理论上就已经被证明不存在。
BASE理论指的是:
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
也就是说:在集中式事务中,我们可以实现完美的ACID。在分布式事务中,在得到分布式的收益下,我们也需要接受不完美。前文我们也提到过,事务处理中,比较难实现的是实时一致性,因此分布式系统的核心理念是:保证数据的最终一致性即可。
在CAP/Base理论的前提下,分布式事务的实现方式,主要分为2PC、补偿事务、TCC。2PC通常是事件驱动架构,补偿事务、TCC通常是通过代码实现,实现难度较高。因此,2PC应用更为广泛。
传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用 ACID transactions。为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。
随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性,也就是我们常说的两阶段提交协议(2PC)。
两阶段提交顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts)。
在阶段1中,coordinator发起一个提议,分别问询各participant是否接受。
在阶段2中,coordinator根据participant的反馈,提交或中止事务,如果participant全部同意则提交,只要有一个participant不同意就中止。
近年来兴起的微服务,也大量使用2PC,即事件驱动的分布式架构。事件驱动架构中,我们会使用到消息中间件如AMQ、Kafka等。
关于AMQ和Kafka容器化实现,可以参考:
https://www.ibm.com/developerworks/cn/cloud/library/cl-lo-building-distributed-message-platform-based-on-openshift/index.html
关于分布式的技术细节,请参考:
六、数据库连接池
在前文中,我们提到了Java应用通过JDBC操作数据库,共分为六大步骤,其中重要的一个环节就是创建访问数据库连接(Connection)。
我们知道,每次创建和断开Connection对象都会消耗一定的时间和I/O资源。为了避免频繁地创建数据库连接,工程师么提出了数据库连接池技术。他负责分配、管理是释放数据库连接,他允许应用程序复用现有的数据库连接,而不是重新建立,如下图所示:
数据库连接池在初始化时,会创建一定数量的数据库连接放到连接池中,当应用程序访问DB时,不是直接创建Connection,而是先从连接池申请Connection(如果池子中没有则新建Conection),使用完毕后,连接池会自动回收。
JDBC提供javax.sql.DataSource接口用于获取DB的Connection。获取Connection有两个方法:
Connection getConenction()
Connection getConnection(String username, String password)
这两个重载的方法都可以获取Connection对象,第一个是通过无参方式创建与DB的连接,第二种方法是通过传入登录信息等内容创建与数据库的连接。
我们通常把实现了javax.sql.DataSource接口的类称为数据源,而数据源中包含数据库连接池。我们用一张形象的图说明数据、数据库、数据源的关系:
现在最常用的数据源有两种:
DBCP
C3P0
七、DBCP
DBCP全称是:DataBase Connection Pool。它是Apache下的开源项目,也是Tomcat使用的连接池组件。
单独使用DBCP的话,需要单独导入一下两个包:
1.commons-dbcp.jar包:DBCP数据源的实现包,包含操作数据库连接信息和数据库连接池初始化信息的方法,并实现了javax.sql.DataSource接口的getConnection()方法。
2.commons-pool.jar:为commons-dbcp.jar中的方法提供支持。
commons-dbcp.jar中包含两个核心的类:BasicDataSource、BasicSourceFactory。
BasicDataSource是BasicSource接口的实现类,包括设置数据源对象的方法:
BasicDataSource可以直接创建数据源对象。我们通过代码进行说明。
我们查看下图红框的标识:我们导入了BasicDataSource类、通过BasicDataSource()方法创建数据源实例bds、通过bds加载mysql的驱动、通过bds设置数据库URL/用户名/密码、将bds赋值ds、通过ds获取数据库连接。
BasicSourceFactory工厂类读取配置文件,然后创建数据对象,然后获取连接对象。
我们先查看配置文件:
我们查看代码,关注红框的标识:
我们导入Properities/DataSource/BasicDataSourceFactory类、创建配置文件对象prop、将配置文件内容加载到prop中、以prop为参数通过BasicSourceFactory创建数据源对象ds、通过ds创建数据库连接对象conn、获取数据库连接信息metaData、通过metaData打印相关信息。
八、C3P0
C3P0是最流行的开源数据库连接池,实现了DataSource数据源接口,支持JDBC2和JDBC3的标准规范。开源框架Hibernate和Spring都是使用C3P0数据源。
C3P0的核心类是ComboPooledDataSource,它是DataSource接口类的实现。
C3P0的核心类ComboPooledDataSource的常用方法如下:
ComboPooledDataSource有两个构造方法:
无参构造方法:ComboPooledDataSource()
有参构造方法:ComboPooledDataSource(String configName)
我们首先利用无参构造方法,展示通过ComboPooledDataSource类直接创建数据源对象:
代码输出结果如下所示:
接下来,我们利用有参构造方法,读取c3p0-config.xml的配置文件(必须是这个名字,而且放到该项目的src目录下),创建数据源对象,然后获取数据库连接对象。
我们首先查看参数文件内容:
我们查看代码:
代码执行结果如下:
九、DButils工具类
为了简单使用JDBC,Apache提供了一个工具类库commons-dbutils。它是操作数据库的一个组件,实现了对JDBC的封装,简化了代码工作量。
commons-dbutils包含两个核心类和一个接口:
两个核心类:
org.apache.commons.dbutils.DButils:主要用于关闭连接、装载JDBC驱动程序之类的常规工作提供方法。
org.apache.commons.dbutils.QueryRunner:主要用于执行增删改查操作。
一个接口:
org.apache.commons.dbutils.ResultSetHandler:主要用于处理ResultSet结果集。
十、DBuTils处理事务
我们如果使用连接池访问DB,就很难保证一个事务只用一个connection。但在不同的Connection中的数据无法共享。这就需要借助ThreadLocal类来实现在一个线程里记录变量。
我们可以生成一个Connection放在线程里,只要这个线程中的任何对象都可以共享这个链接,当线程结束后就删除这个Connection。
接下来我们模拟银行间的转账事务,来理解DBUtils的作用。
首先创建数据库表acoount作为账务记录表,插入数据:
查看account数据库表,内容如下,我们看到a账户有1000、B账户有1000:
创建实体类Account,通过以下代码我们可以看出这个实体类包含对Account表操作的方法(get/set)
创建类JDBCUtils这个类一共有三大段代码,该类封装了创建Connection、开启事务关闭事务的方法:
本类代码第二段定义了开始开启事务和提交事务的方法。我们看到开启事务前必须要先获取Connection。我们看到提交事务获取Connection是从threadLocal获取的。
第三段代码实现了回滚事务和关闭事务的方法。我们看到回滚和关闭事务前的Connection是从threadLocal获取的。
创建AccountDao类,该类封装了转账所需的数据库操作,包括查询用户、转入、转出操作,代码实现如下,下面代码实现了查询和Update SQL,下面代码导入了dbutils类:
创建Business类,这个类包含转账过程的逻辑方法,导入了封装事务操作的JDBCUtils类和封装数据库操作的AccountDao类,完成后转账操作,我们可以看到下面代码定义了一个名为Transfer的静态方法:
调用transfer方法,并注入参数,如下图所示:
执行结果如下,我们看到转账成功: