posts - 78, comments - 34, trackbacks - 0, articles - 1
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

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,这个就不多说了。

今天的内容也就事务的隔离性有些难度吧!一般也不会去用那最高安全级别,这一级别在银行中比较常用。

正如老方所说,是不是感觉越学越简单了!确实如此,我想大家应该也对此有感觉。不知不觉明天又休息了!

加油!


评论

# re: 2009-12-02传智播客 数据库&mdash;&mdash;JDBC开发 连接池与事务  回复  更多评论   

2010-06-14 09:25 by peixin.xu
只能说你很强大

只有注册用户登录后才能发表评论。


网站导航: