JDBC学习之事务和连接池

上一篇写了一些基本数据库操作,以及批处理、调用存储过程等内容,这一次主要是使用事务以及使用连接池来提高效率,当然最好还是选用比较成熟的连接池,这些在框架中应该都已经集成了,但是现在不是没学嘛~多了解一点也是有好处的
到这里 JDBC 基本就差不多了,如果还看到什么知识点的话就再补充一篇

使用事务

关于什么是事务,以及有什么特点,我在这篇文章中更新过了,下面就主要说说在 JDBC 中如何使用事务,还是以 MySQL 为例,其他的也都一致,就是注意下隔离级别的问题,像 Oracle 就只支持 2 种;首先来看下最基本的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
// 关闭自动提交
conn.setAutoCommit(false);
String sql1 = "update users set rmb = rmb-100 where name='lolicon'";
String sql2 = "update users set rmb = rmb+100 where name='loli'";

st = conn.prepareStatement(sql1);
st.executeUpdate();

st = conn.prepareStatement(sql2);
st.executeUpdate();
// 提交事务
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.release(conn, st, rs);
}
}

主要操作的是 conn 对象,首先我们要关闭它的自动提交,否则每发送一条 SQL 会立即完成,关闭后相当于开启了一个事务,无论执行多少次 SQL 只要不进行 commit ,数据库就会进行回滚操作

下面再说下手动设置事务的回滚点,并且手动回滚操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static void custPoint() throws SQLException {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
Savepoint sp = null;
try {
conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);
String sql1 = "update users set rmb = rmb-100 where name='lolicon'";
String sql2 = "update users set rmb = rmb+100 where name='loli'";

st = conn.prepareStatement(sql1);
st.executeUpdate();
// 设置回滚点
sp = conn.setSavepoint();
st = conn.prepareStatement(sql2);
st.executeUpdate();
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
// 手动进行回滚,记得进行 commit
if (conn != null) {
conn.rollback(sp);
conn.commit();
}
}finally {
JdbcUtils.release(conn,st,rs);
}
}

采用的方式是设置一个回滚点 Savepoint ,当然应该还有其他方式的….先说这一种

然后是手动设置隔离级别,就是一句话的事:

1
2
3
4
5
6
7
8
private static void test() throws SQLException {
Connection conn = JdbcUtils.getConnection();
// 设置隔离级别
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
conn.setAutoCommit(false);
//TODO
JdbcUtils.release(conn,null,null);
}

补充一点,产生 conn 的方法还可以使用工厂模式来生产 Dao
它能使各层之间能达到完全解耦的目的,但是相应的代码的复杂度会增加
如果项目确定不会更换 Dao 层,那么其实不使用工厂模式比较好
代码参考: Github

使用连接池

首先,要明确数据库能创建的连接数(也就是 conn)是非常有限的,并且创建出来也很不容易,所以,每次操作数据库的时候就让数据库给创建连接效率是非常低的,于是就有了连接池的概念
就是说,事先先创建好一批连接,存到一个“池”里,当需要的时候就从这个池里取,用完后不要释放而是再放回这个池里,这样会极大的提高效率
思路大概就是这样了….

下面来手写个连接池,当然是最基本的;简单说就是实现 DataSource 接口,这个接口只有两个方法,其他都是继承过来的,可以简单的认为实现了DataSource 接口的就是一个连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class JdbcPool implements DataSource {
// 设计到频繁删改 使用链表结构效率好一点
private static LinkedList<Connection> mList = new LinkedList<>();
private static Properties prop = new Properties();
static {
try {
prop.load(JdbcUtils.class.getClassLoader().getResourceAsStream("db.properties"));
Class.forName(prop.getProperty("driver"));
// 获取 10 个连接
for (int i = 0; i < 10; i++) {
mList.add(DriverManager.getConnection(prop.getProperty("url"), prop.getProperty("username"), prop.getProperty("password")));
}
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

@Override
public Connection getConnection() throws SQLException {
if (mList.size() <= 0) {
throw new RuntimeException("数据库正忙....请稍后再获取");
}
Connection conn = mList.removeFirst();
// 进行装饰,使其 close 的时候再放回池里
MyConn myConn = new MyConn(conn);
return myConn;
}

/**
* 使用装饰模式增强 close,使其关闭的时候添加回连接池中
* 有点繁琐,最好还是使用 动态代理(aop 面向切面,拦截技术)
*
* 装饰步骤
* 1.定义一个类,实现被增强类相同的接口
* 2.定义一个变量,记住被增强对象
* 3.定义一个构造函数,接收被增强对象
* 4.覆盖想增强的方法
* 5.对于不想增强的方法,直接调用目标对象的方法
*/
class MyConn implements Connection{
private Connection conn;

public MyConn(Connection conn) {
this.conn = conn;
}

@Override
public void close() throws SQLException {
// 进行自定义
mList.add(conn);
}

@Override
public Statement createStatement() throws SQLException {
return this.conn.createStatement();
}

@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return this.conn.prepareStatement(sql);
}

.....
}
// 还有很多方法,没用到省略了
.....

DBCP连接池

对于新手,自己写的连接池是很不放心啊,所以还是用比较成熟的开源的比较好,比如 BDCP 和 C3P0
哦,对了 DBCP 是 Apache 的,下面是使用的参考代码,记得导入相关的包,没记错的话应该是需要三个包,一个是 DBCP 的还需要 logging 和 pool2 的包,有点麻烦,不过都是 Apache 家的,在 Apache 的官网都可以找到
主包地址:https://commons.apache.org/proper/commons-dbcp/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class JdbcUtilsDBCP {
private static DataSource ds = null;
private static Properties prop = new Properties();
// 使用静态代码块保证只加载一次
static {
try {
prop.load(JdbcUtilsDBCP.class.getClassLoader().getResourceAsStream("dbcp.properties"));
ds = BasicDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

public static Connection getConnection() throws SQLException {
return ds.getConnection();
}

public static void release(Connection conn, Statement st, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}

顺便贴下 DBCP 的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
########DBCP配置文件##########
#驱动名
driverClassName=com.mysql.jdbc.Driver
#url
url=jdbc:mysql://127.0.0.1:3306/test
#用户名
username=Loli
#密码
password=123456
#初试连接数
initialSize=10
#最大活跃数
maxTotal=30
#最大idle数
maxIdle=10
#最小idle数
minIdle=5
#最长等待时间(毫秒)
maxWaitMillis=1000
#程序中的连接不使用后是否被连接池回收(该版本要使用removeAbandonedOnMaintenance和removeAbandonedOnBorrow)
#removeAbandoned=true
removeAbandonedOnMaintenance=true
removeAbandonedOnBorrow=true
#连接在所指定的秒数内未使用才会被删除(秒)(为配合测试程序才配置为1秒)
removeAbandonedTimeout=1

C3P0连接池

使用步骤和 DBCP 类似,不过貌似效率更高一些
官网地址:http://www.mchange.com/projects/c3p0/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class JdbcUtilsC3P0 {
private static ComboPooledDataSource ds = null;
// 使用静态代码块保证只加载一次
static {
try {
ds = new ComboPooledDataSource();
/*
// 如果采用的是配置文件,注释的内容可以不写
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://115.152.254.541:3306/test");
ds.setUser("Loli");
ds.setPassword("123456");
// 设置初始连接池的大小!
ds.setInitialPoolSize(2);
// 设置连接池的最小值!
ds.setMinPoolSize(1);
// 设置连接池的最大值!
ds.setMaxPoolSize(10);
// 设置连接池中的最大 Statements 数量!
ds.setMaxStatements(50);
// 设置连接池的最大空闲时间!
ds.setMaxIdleTime(60);
*/
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

public static Connection getConnection() throws SQLException {
return ds.getConnection();
}

public static void release(Connection conn, Statement st, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}

如果在 src 下有配置文件直接 new 就可以了,如果没有那就只能手动指定了,配置文件统一命名为 c3p0-config.xml ,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8" ?>
<c3p0-config>
<default-config>
<property name="jdbcUrl">jdbc:mysql://115.159.234.122:3306/test</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="user">Loli</property>
<property name="password">123456</property>
   <!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
<property name="acquireIncrement">3</property>
   <!-- 初始化数据库连接池时连接的数量 -->
<property name="initialPoolSize">2</property>
<!-- 数据库连接池中的最小的数据库连接数 -->
<property name="minPoolSize">1</property>
<!-- 数据库连接池中的最大的数据库连接数 -->
<property name="maxPoolSize">10</property>
</default-config>

<!-- This app is massive! -->
<named-config name="intergalactoApp">
<property name="acquireIncrement">50</property>
<property name="initialPoolSize">100</property>
<property name="minPoolSize">50</property>
<property name="maxPoolSize">1000</property>
<!-- intergalactoApp adopts a different approach to configuring statement caching -->
<property name="maxStatements">0</property>
<property name="maxStatementsPerConnection">5</property>
</named-config>
</c3p0-config>

default 对应空构造函数,如果你还配置了其他的,比如上面的就是叫 named ,在 new ComboPooledDataSource 的时候把名字作为参数传入就好了,这样更加灵活

Tomcat自带连接池

其实就是用的 DBCP,因为 DBCP 是 Apache 的,Tomcat 也是 Apache 的,都是一家子嘛~~
使用自带的连接池的步骤在 Tomcat 的文档中写的也算是很清楚了,见:JNDI Resources 目录下

因为使用自带的连接池需要配 Context ,Context 的配置方式一共有 5 种 (:应该是
见:官方在线文档
除去前面介绍 Tomcat 时说的几种,还有一种是在工程的 Web 目录下新建 META-INF 目录,然后再新建 context.xml 文件,在这个文件中进行配置
其实这种方式最终会复制一份到 conf\Catalina\localhost 下,一般以应用名开头,所以清理项目的时候记得手动删除

好了,继续说,首先按照文档先要把驱动拷到服务器的 lib 文件夹,然后进行配置 Context 就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
<Context ...>
...
<Resource name="jdbc/EmployeeDB"
auth="Container"
type="javax.sql.DataSource"
username="dbusername"
password="dbpassword"
driverClassName="org.hsql.jdbcDriver"
url="jdbc:HypersonicSQL:database"
maxActive="8"
maxIdle="4"/>
...
</Context>

获取链接的示例代码为:

1
2
3
4
5
6
7
8
9
// 初始化 JNDI
Context initCtx = new InitialContext();
// 获取 JNDI
Context envCtx = (Context) initCtx.lookup("java:comp/env");
// 获取连接池
DataSource ds = (DataSource) envCtx.lookup("jdbc/EmployeeDB");

// 获取连接
Connection conn = ds.getConnection();

文档为啥会在 JNDI 目录下呢,因为数据库池(DataSource)保存在 web 服务器的 JNDI 容器中(当然也是可以存其他东西的)

所以说,服务器传对象给 Servlet 其实又多了一种方式,那就放在 JNDI 容器中,需要的时候向 JNDI 中获取就行了

JNDI科普

什么是 JNDI ?

JNDI是 Java 命名与目录接口(Java Naming and Directory Interface),在J2EE规范中是重要的规范之一.

JNDI 是 Java 平台的一个标准扩展,提供了一组接口、类和关于命名空间的概念。如同其它很多 Java 技术一样,JDNI 是 provider-based 的技术,暴露了一个 API 和一个服务供应接口(SPI)。
这意味着任何基于名字的技术都能通过 JNDI 而提供服务,只要 JNDI 支持这项技术。
JNDI 目前所支持的技术包括 LDAP、CORBA Common Object Service(COS)名字服务、RMI、NDS、DNS、Windows 注册表等等。
很多 J2EE 技术,包括 EJB 都依靠 JNDI 来组织和定位实体。可以把它理解为一种将对象和名字捆绑的技术,对象工厂负责生产出对象,这些对象都和唯一的名字绑在一起,外部资源可以通过名字获得某对象的引用。

简单来说就是,原来我们写代码需要在代码中指定数据库的连接、用户名、密码等信息,现在只需要在 XML 配置文件中指定就可以了,达到了解耦的目的;现在这些问题都由 J2EE 容器来配置和管理,程序员只需要对这些配置和管理进行引用即可。
J2EE 规范要求所有 J2EE 容器都要提供 JNDI 规范的实现。JNDI 是通过资源的名字来查找的,资源的名字在整个j2ee应用中(j2ee 容器中)是唯一的

JTA与EJB科普

java Transaction API(Java事务 API)完整的名称应该是:Java Transaction API(Application Programming Interface)

JTA Transaction 是指由 J2EE Transaction manager 去管理的事务。其最大的特点是调用 UserTransaction 接口的 begin,commit 和 rollback 方法来完成事务范围的界定,事务的提交和回滚。
JTATransaction 可以实现同一事务对应不同的数据库,但是它仍然无法实现事务的嵌套。

就是我们所说的全局事务


顺便再补充下什么是 EJB 吧:

企业级JavaBean(Enterprise JavaBean, EJB)是一个用来构筑企业级应用的服务器端可被管理组件。

用通俗话说,EJB 就是:”把你编写的软件中那些需要执行制定的任务的类,不放到客户端软件上了,而是给他打成包放到一个服务器上了”。
是的,没错!EJB 就是将那些”类”放到一个服务器上,用 C/S 形式的软件客户端对服务器上的”类”进行调用。

EJB 包含的 3 种 bean 是:session bean(会话 bean), entity bean(实体 bean), message bean(消息 bean)

更详细的内容可参考:http://blog.csdn.net/cuiran/article/details/40950487

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~