Today is JDBC高级部分,课程的主要内容有连接池与事务。这都是在应用开发中比较常用,比较重要的。
一、使用配置文件优化JDBCUtil,使用工厂模式隔离DAO层:
昨天有说过将对数据库获取连接和释放的操作单封装到一个类中,如:
import java.sql.*;
import cn.itcast.cc.exception.JDBCUtilException;
/**
* JDBC工具类
*
* @author Administrator
*
*/
public class JDBCUtil {
// 连接数据库时所需要的参数
private static String url = "jdbc:mysql://localhost:3306/jdbc";
private static String username = "root";
private static String password = "root";
// 类被加载时就加载驱动
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
// 获取连接
public static Connection getConnection() throws JDBCUtilException {
try {
return DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
throw new JDBCUtilException(e);
}
}
// 释放连接等资源,下面是一种健康的释放方式
public static void release(ResultSet rs, Statement sta, Connection conn)
throws JDBCUtilException {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new JDBCUtilException(e);
}
rs = null;
}
if (sta != null) {
try {
sta.close();
} catch (SQLException e) {
throw new JDBCUtilException(e);
}
sta = null;
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new JDBCUtilException(e);
}
conn = null;
}
}
}
从上面我们看到其中装载数据库驱动和获取数据库连接的代码:
Class.forName("com.mysql.jdbc.Driver");
DriverManager.getConnection(url, username, password); |
我们使用了固定的url,它是一个成员。在以后的开发中,如果要使用其他的数据库或者数据库的用户名密码被改变了,我们还需要手动的修改上边的代码,重新编译。这是一种不良的设计方法,为了使我们的代码与数据库分离,我们可以将这些参数配置到配置文件中。这样在需要使用时去读取配置文件中的对应值即可。以后换数据库或更改了用户名,直接修改一下配置文件即可。
昨天有提到过DAO层,DAO层专门用于处理对数据库的CURD操作等,比如,添加用户、查找用户、修改用户、删除用户,我们可以把这些操作封装到一个类中(UserDao.java),它所处位置就是DAO层。比如,用于处理用户注册的Servlet,在这个Servlet中直接实例化一个UserDao对象。然后调用userDaoObj.add(userbean);方法即可实现用户的注册。
如果我想换一种数据存储方式,比如配置文件,我只将用户信息保存在配置文件中。这样我就需要修改UserDao.java,但有时又需要UserDao.java这个功能类怎么办?我只能重新创建一个专门处理配置文件数据的类(UserDaoPro.java)。这样我又需要修改Servlet中的代码,将实例化UserDao的代码修改为实例化UserDaoPro对象。那UserDaoPro与UserDao的接口是不是相同的呢?如果不相同麻烦就大了,JAVA是提倡使用接口编程的,就是为了实现接口的统一化。
可见,上面的这种实现方式是存在问题的。即使接口统一,我们还需要修改Servlet中的代码。此时工厂模式派上了用场,还记得工厂模式吧!是这样的:
1.定义一个UserDao接口,统一对用户操作的接口。
2.定义一个UserDaoFactory的工厂类,专门生产UserDao对象。
3. UserDaoFactory工厂从配置文件中读取实现了UserDao接口的类名称。使用此类生产产品——UserDao对象。
4.在Servlet中使用UserDaoFactory工厂来创建需要的UserDao对象。
这样就实现了DAO层与Servlet的完全分离!完美!
二、数据库连接池:
每当有一个新的连接发生时,数据库就需要创建一个Connection对象来处理连接。访问结束后,Connection对象被释放。数据库创建一个Connection对象,是十分耗时且消耗较大的服务器资源。试想,如果有500个用户同时访问数据库,数据库同时创建500个Connection将会是什么样子!因此,连接池这一技术诞生了。
连接池,在服务器加载WEB应用后。WEB应用会自动创建一个连接池,池中包含多个Connection对象。每当有新的连接请求时,便从这个池中拿出一个Connection对象用于处理连接,使用完成后,便还回给这个池子。下面代码为连接池的实现原理:
import java.io.*;
import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
import javax.sql.DataSource;
/**
* 自己编写的简单连接池类,单例模式实现。
*
* @author Administrator
*
*/
public class JDBCUtil implements DataSource {
private static LinkedList<Connection> conns = new LinkedList<Connection>();
private static JDBCUtil myjdbcutil = new JDBCUtil();
private JDBCUtil() {
// 取配置文件
InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream(
"cn/itcast/cc/db/myjdbc.properties");
// 装载配置文件
Properties pro = new Properties();
try {
pro.load(in);
} catch (IOException e) {
e.printStackTrace();
}
// 取出配置项
String driver = pro.getProperty("driverClassName");
String url = pro.getProperty("url");
String username = pro.getProperty("username");
String password = pro.getProperty("password");
int initialSize = Integer.parseInt(pro.getProperty("initialSize"));
// 加载驱动,填充连接池
try {
// 常用的数据库,驱动类被加载时,都会自已加载驱动。
Class.forName(driver);
for (int i = 0; i < initialSize; i++) {
conns.add(DriverManager.getConnection(url, username, password));
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 返回单例的实例
public static JDBCUtil getInstance() {
return myjdbcutil;
}
public Connection getConnection() throws SQLException {
if (conns.size() > 0) {
final Connection conn = conns.pop();
// 此处需要使用动态代理技术,因为返回的Connection对象在关闭时需要被收回到LinkedList中。
// 动态代理,增强Connection.colse()方法,将它回收到LinkedList中。但不销毁!
return (Connection) Proxy.newProxyInstance(JDBCUtil.class
.getClassLoader(), conn.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
// 如果是close方法,则将conn回收到List中。
if (method.getName().equals("close")) {
conns.push(conn);
return null;
}
return method.invoke(conn, args);
}
});
}
throw new RuntimeException("服务器忙,请稍后连接!");
}
}
有很多服务器为用户提供了DataSource(数据源)的实现。DataSource中包含了连接池的实现。也有一些第三方组织单独提供了连接池的实现:DBCP、C3P0。所以,在以后的开发中,我们可以直接使用这些资源。
著名的DBCP比较常用,使用DBCP必须引入Commons-dbcp.jar和Commons-pool.jar。Tomcat中的连接池也是使用DBCP实现的。DBCP的使用非常简单:
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSourceFactory;
public class DBCPUtil {
private static DataSource ds = null;
static {
try {
// 取配置文件
InputStream in = JDBCUtil.class.getClassLoader()
.getResourceAsStream(
"cn/itcast/cc/db/dbcpconfig.properties");
// 装载配置文件
Properties pro = new Properties();
pro.load(in);
// 创建数据源
ds = BasicDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}
}
// 获取连接
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
}
其中的配置文件“dbcpconfig.properties”:
#连接设置
#驱动器
driverClassName=com.mysql.jdbc.Driver
#数据库连接URL
url=jdbc:mysql://localhost:3306/jdbc
#数据库用户名
username=root
#数据库密码
password=root
#初始连接池数量
initialSize=10
#最大连接数量
maxActive=50
#最大空闲连接
maxIdle=20
#最小空闲连接
minIdle=5
#超时等待时间以毫秒为单位 6000毫秒/1000等于60秒
maxWait=60000
#JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;]
#注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
connectionProperties=useUnicode=true;characterEncoding=UTF8
#指定由连接池所创建的连接的自动提交(auto-commit)状态。
defaultAutoCommit=true
#driver default 指定由连接池所创建的连接的只读(read-only)状态。
#如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
defaultReadOnly=
#driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
#可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
defaultTransactionIsolation=READ_UNCOMMITTED |
上面是直接使用Apache提供的DBCP实现的连接池,接下来看一下使用JNDI技术在Tomcat中配置连接池,在Tomcat服务的conf\context.xml文件中添加:
<Context>
<Resource name="jdbc/datasource" auth="Container"
type="javax.sql.DataSource" username="root" password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/jdbc"
maxActive="8" maxIdle="4"/>
</Context> |
Name: 对象在JNDI容器中的名称。
Auth:所使用的容器。
Type:对象类型。
Username:数据库用户名。
Password:数据库密码。
driverClassName:数据库驱动类。
url:连接数据库的URL。
maxActive:最大连接数。
maxIdle:最大空闲连接数。
注意其中的数据库驱动,对应的jar包必须放置到Tomcat目录下的lib目录中。这样服务器才能找得到。
在WEB应用中获取数据库连接的代码:
// 初始化JNDI环境
Context initCtx = new InitialContext();
// 获取JNDI容器
Context envCtx = (Context) initCtx.lookup("java:comp/env");
// 获取数据源
DataSource dataSource = (DataSource)envCtx.lookup("jdbc/datasource");
// 获取数据库连接
dataSource.getConnection(); |
JNDI是一个对象容器,服务器根据配置文件提供的信息。创建这些对象,并按照指定的名称保存在容器中。WEB应用,无论在何处都可以根据名称获取在JNDI容器中的对象。
三、数据库事务管理:
什么是事务?老方有这样的面试题。事务指逻辑上的一组操作,这组操作要不全部生效,要不全部失效。在控制台中操作事务的命令:
start transaction 开启事务
Rollback 回滚事务
Commit 提交事务
set transction isolation level 设置事务隔离级别
select @@tx_isolation 查询当前事务隔离级别 |
事务的特性(ACID),这在面试中是比较常见的。老方一再强调,一定要记下来!OK,让我们看一看。
事务的原子性(Automicity),它更像是事务的定义。就是逻辑上的一组操作,不可分割,要么全部生效,要么全部失效。
事务的一致性(Consistency),老方的PPT有中说:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。比如,银行转账,A-B。A账户中减少100元,B帐户中增加一百元。这看起来比较好理解,但让我有点疑问的是,如果从数据库中删除一个用户呢?难道不用事务处理吗?所以我对这个一致性的理解除了老方说的,还有只要事务处理符合正确的逻辑,就是一致性。
隔离性(Isolation),如果多个用户同时访问数据库,同时访问一张表。这样会造成一些访问错误,所以有了隔离性这一定义。将多个并发事务之间要相互隔离。
持久性(Durability),事务一旦被提交,它对数据库中数据的改变是永久性的,无论其他操作或数据库故障不会对它有任何影响。
在JDBC中使用事务的语句:
// 开启事务
conn.setAutoCommit(false);
// 回滚事务
conn.rollback();
// 提交事务
conn.commit();
// ...
// 设置保存点
sp = conn.setSavepoint();
// 回滚到指定保存点
conn.rollback(sp); |
方老师使用了一个经典的案例,来讲解事务处理——银行转账系统!
例,用户A转账10000元给用户B。应该是A账户中减少10000元,B账户中增加10000元。那么在A提交了10000元后,数据库发生问题(比如断电),那B帐户增加10000元的语句还没有执行。使用事务可以很好解决这一问题:
1. 开启事务
2. A帐户减少10000元
3. 中间发生错误,回滚事务
4. B帐户增加10000元
5. 提交事务
OK,在上同这一过程中,如果没有执行到第5步。数据库的内容就不会发生改变。在此有必要解释一下回滚事务,回滚事务在提交事务后执行没有什么效果,回滚事务在提交事务之前执行,会擦除在开启事务之后执行的SQL语句。
上面是针对单一事务进行的处理,在并发事务中会遇到一些问题。设置事务的隔离级别可以很好的解决这一问题。
首先我们来看一下并发事务容易产生的问题:
脏读:
例,用户A账户减少10000元,用户B账户增加了10000元,但A还没有提交事务。此时,B去查询它的账户发现A已经给自己转账了10000元。然后,A回滚了事务。B被A给骗了,这就是脏读,B读到的是未提交的数据。我虽对数据库事务的实现原理不了解(事务是通过日志处理的),我打个比方就更好理解这一原因了。数据库对所有用户维护了一个虚表,在开启事务中,提交事务前对数据的修改,都是修改的虚表,用户查询的也是虚表的内容,当执行了提交事务操作后,将虚表中被修改的内容保存到数据库。实际技术细节就不深入了!
不可重复读:
例,用户A原账户内容为100万元,此时银行将数据显示到电脑屏幕和保存到文件中。银行工作人员在电脑屏幕上看到的是100万元,但此时,用户A向账户里又存了100万元。结果保存到文件的是200万元。到是100万元还是200万元?工作人员有些迷惑。与脏读相反的是,脏读是读取了前一事务未提交的数据,不可重复读读取的是前一事务提交了的事务。注意,不可重复读针对的是某一记录!
虚读:
例,银行要统计A的报表。A的账户中有200万元,屏幕上报表显示的总额是200万元,此时,用户A向账户中又存入了100万元。报表打印出来的总额就是300万元。工作人员一对比,到底哪个是正确的?注意,虚读针对的是整个表。
不可重复读和虚读,有点难理解。这看起来不是什么问题,如果发生这一问题,工作人员就刷新一下表呗!如果工作人员处理的是1万个用户的报表,同时不幸运的是这1万个用户都发生了虚读的问题。那工作人员能受了吗?你可能还会说那就以打印的报表为准呗!问题是,可能屏幕上的那个才是正确的,这样就要重新打印报表。所以为了使问题得以简单的解决,事务的隔离性发挥了它的作用!
处理上边的三种并发事务产生的问题,数据库定义了四种隔离级别:
Serializable:可避免脏读、不可重复读、虚读情况的发生。
Repeatable read:可避免脏读、不可重复读情况的发生。
Read committed:可避免脏读情况发生。
Read uncommitted:最低级别,以上情况均无法保证。 |
Serializable,最高安全级别。设置了此级别,当一个事务访问了某一表时,此表就会被封死。其他事务对此表的访问就会被挂起,直到上一事务提交后才执行一个事务。OK,这看起来十分容易理解。
Repeatable read,让我再小深入一下,当每个事务线程访问同一表时,数据库针对每个线程维护了一张虚拟表。此时各线程看到的都是原始的数据内容,对表数据的修改相互不发生影响,即使事务被提交了。数据库可能为每个线程维护了一张虚拟表吗?当然不可以,我想这只是为了便于理解。技术细节不深入研究!
Read committed,数据库对每个用户的查询显示的都是原始内容(真实内容)。如果某些用户对此表的事务没有提交就不会影响原始内容。所以其他用户查看到的都是原始内容或提交了的数据内容。
Read uncommitted,这个就不多说了。
今天的内容也就事务的隔离性有些难度吧!一般也不会去用那最高安全级别,这一级别在银行中比较常用。
正如老方所说,是不是感觉越学越简单了!确实如此,我想大家应该也对此有感觉。不知不觉明天又休息了!
加油!