JavaWeb开发与代码的编写(十六)
JDBC进行批处理
在实际的项目开发中,有时候需要向数据库发送一批SQL语句执行,这时应避免向数据库一条条的发送执行,而应采用JDBC的批处理机制,以提升执行效率。
JDBC实现批处理有两种方式:statement和preparedstatement
使用Statement完成批处理
1、使用Statement对象添加要批量执行SQL语句,如下:
Statement.addBatch(sql1);
Statement.addBatch(sql2);
Statement.addBatch(sql3);
2、执行批处理SQL语句:Statement.executeBatch();
3、清除批处理命令:Statement.clearBatch();
使用Statement完成批处理范例
1、编写测试的SQL脚本创建表
create table testbatch
(
id int primary key,
name varchar(20)
);
2、编写测试代码,如下所示:
package me.gacl.demo;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import me.gacl.utils.JdbcUtils;
import org.junit.Test;
/**
* @ClassName: JdbcBatchHandleByStatement
* @Description: 使用Statement实现JDBC批处理操作
*
*/
public class JdbcBatchHandleByStatement {
@Test
public void testJdbcBatchHandleByStatement(){
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try{
conn = JdbcUtils.getConnection();
String sql1 = "insert into testbatch(id,name) values(1,'aaa')";
String sql2 = "insert into testbatch(id,name) values(2,'bbb')";
String sql3 = "insert into testbatch(id,name) values(3,'CCC')";
String sql4 = "insert into testbatch(id,name) values(4,'DDD')";
String sql5 = "update testbatch set name='gacl' where id=1";
String sql6 = "insert into testbatch(id,name) values(5,'FFF')";
String sql7 = "delete from testbatch where id=2";
st = conn.createStatement();
//添加要批量执行的SQL
st.addBatch(sql1);
st.addBatch(sql2);
st.addBatch(sql3);
st.addBatch(sql4);
st.addBatch(sql5);
st.addBatch(sql6);
st.addBatch(sql7);
//执行批处理SQL语句
st.executeBatch();
//清除批处理命令
st.clearBatch();
}catch (Exception e) {
e.printStackTrace();
}finally{
JdbcUtils.release(conn, st, rs);
}
}
}
采用Statement.addBatch(sql)方式实现批处理的优缺点
采用Statement.addBatch(sql)方式实现批处理:
优点:可以向数据库发送多条不同的SQL语句。
缺点:SQL语句没有预编译。
当向数据库发送多条语句相同,但仅参数不同的SQL语句时,需重复写上很多条SQL语句。例如:
Insert into user(name,password) values('aa','111');
Insert into user(name,password) values('bb','222');
Insert into user(name,password) values('cc','333');
Insert into user(name,password) values('dd','444');
使用PreparedStatement完成批处理
使用PreparedStatement完成批处理范例
测试代码如下:
package me.gacl.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import me.gacl.utils.JdbcUtils;
import org.junit.Test;
/**
* @ClassName: JdbcBatchHandleByStatement
* @Description: 使用prepareStatement实现JDBC批处理操作
*
*/
public class JdbcBatchHandleByPrepareStatement {
@Test
public void testJdbcBatchHandleByPrepareStatement(){
long starttime = System.currentTimeMillis();
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try{
conn = JdbcUtils.getConnection();
String sql = "insert into testbatch(id,name) values(?,?)";
st = conn.prepareStatement(sql);
for(int i=1;i<1000008;i++){ //i=1000 2000
st.setInt(1, i);
st.setString(2, "aa" + i);
st.addBatch();
if(i%1000==0){
st.executeBatch();
st.clearBatch();
}
}
st.executeBatch();
}catch (Exception e) {
e.printStackTrace();
}finally{
JdbcUtils.release(conn, st, rs);
}
long endtime = System.currentTimeMillis();
System.out.println("程序花费时间:" + (endtime-starttime)/1000 + "秒!!");
}
}
采用PreparedStatement.addBatch()方式实现批处理的优缺点
采用PreparedStatement.addBatch()实现批处理
优点:发送的是预编译后的SQL语句,执行效率高。
缺点:只能应用在SQL语句相同,但参数不同的批处理中。因此此种形式的批处理经常用于在同一个表中批量插入数据,或批量更新表的数据。
关于JDBC批处理的内容就总结这么多。
MySQL数据库自动生成的主键
测试脚本如下:
create table test1
(
id int primary key auto_increment,
name varchar(20)
);
测试代码:
package me.gacl.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import me.gacl.utils.JdbcUtils;
public class Test {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try{
conn = JdbcUtils.getConnection();
String sql = "insert into test1(name) values(?)";
st = conn.prepareStatement(sql);
st.setString(1, "aaa");
st.executeUpdate();
//获取数据库自动生成的主键
rs = st.getGeneratedKeys();
if(rs.next()){
System.out.println(rs.getInt(1));
}
}catch (Exception e) {
e.printStackTrace();
}finally{
JdbcUtils.release(conn, st, rs);
}
}
}
事物
事务的概念
事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功。
例如:A——B转帐,对应于如下两条sql语句
update from account set money=money+100 where name='B';
update from account set money=money-100 where name='A';
MySQL数据库中操作事务命令
1、编写测试SQL脚本,如下:
/*创建账户表*/
create table account(
id int primary key auto_increment,
name varchar(40),
money float
);
/*插入测试数据*/
insert into account(name,money) values('A',1000);
insert into account(name,money) values('B',1000);
insert into account(name,money) values('C',1000);
下面我们在MySQL数据库中模拟A——B转帐这个业务场景
开启事务(start transaction)
使用"start transaction"开启MySQL数据库的事务,如下所示:
我们首先在数据库中模拟转账失败的场景,首先执行update语句让A用户的money减少100块钱,如下图所示:
然后我们关闭当前操作的dos命令行窗口,这样就导致了刚才执行的update语句的数据库的事务没有被提交,那么我们对A用户的修改就不算是是真正的修改了,下次在查询A用户的money时,依然还是之前的1000,如下图所示:
提交事务(commit)
下面我们在数据库模拟A——B转账成功的场景
我们手动提交(commit)数据库事务之后,A——B转账100块钱的这个业务操作算是真正成功了,A账户中少了100,B账户中多了100。
回滚事务(rollback)
通过手动回滚事务,让所有的操作都失效,这样数据就会回到最初的初始状态!
JDBC中使用事务
当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列的JDBC控制事务语句
- Connection.setAutoCommit(false);//开启事务(start transaction)
- Connection.rollback();//回滚事务(rollback)
- Connection.commit();//提交事务(commit)
JDBC使用事务范例
在JDBC代码中演示银行转帐案例,使如下转帐操作在同一事务中执行
"update account set money=money-100 where name='A'"
update account set money=money+100 where name='B'
代码如下:
1 package me.gacl.demo;
2
3 import java.sql.Connection;
4 import java.sql.PreparedStatement;
5 import java.sql.ResultSet;
6 import java.sql.SQLException;
7 import me.gacl.utils.JdbcUtils;
8 import org.junit.Test;
9
10 /**
11 * @ClassName: TransactionDemo1
12 * @Description:
13 * JDBC中使用事务来模似转帐
14 create table account(
15 id int primary key auto_increment,
16 name varchar(40),
17 money float
18 );
19 insert into account(name,money) values('A',1000);
20 insert into account(name,money) values('B',1000);
21 insert into account(name,money) values('C',1000);
24 *
25 */
26 public class TransactionDemo1 {
27
28 /**
29 * @Method: testTransaction1
30 * @Description: 模拟转账成功时的业务场景
32 *
33 */
34 @Test
35 public void testTransaction1(){
36 Connection conn = null;
37 PreparedStatement st = null;
38 ResultSet rs = null;
39
40 try{
41 conn = JdbcUtils.getConnection();
42 conn.setAutoCommit(false);//通知数据库开启事务(start transaction)
43 String sql1 = "update account set money=money-100 where name='A'";
44 st = conn.prepareStatement(sql1);
45 st.executeUpdate();
46 String sql2 = "update account set money=money+100 where name='B'";
47 st = conn.prepareStatement(sql2);
48 st.executeUpdate();
49 conn.commit();//上面的两条SQL执行Update语句成功之后就通知数据库提交事务(commit)
50 System.out.println("成功!!!"); //log4j
51 }catch (Exception e) {
52 e.printStackTrace();
53 }finally{
54 JdbcUtils.release(conn, st, rs);
55 }
56 }
57
58 /**
59 * @Method: testTransaction1
60 * @Description: 模拟转账过程中出现异常导致有一部分SQL执行失败后让数据库自动回滚事务
63 */
64 @Test
65 public void testTransaction2(){
66 Connection conn = null;
67 PreparedStatement st = null;
68 ResultSet rs = null;
69
70 try{
71 conn = JdbcUtils.getConnection();
72 conn.setAutoCommit(false);//通知数据库开启事务(start transaction)
73 String sql1 = "update account set money=money-100 where name='A'";
74 st = conn.prepareStatement(sql1);
75 st.executeUpdate();
76 //用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交,此时数据库会自动执行回滚操作
77 int x = 1/0;
78 String sql2 = "update account set money=money+100 where name='B'";
79 st = conn.prepareStatement(sql2);
80 st.executeUpdate();
81 conn.commit();//上面的两条SQL执行Update语句成功之后就通知数据库提交事务(commit)
82 System.out.println("成功!!!");
83 }catch (Exception e) {
84 e.printStackTrace();
85 }finally{
86 JdbcUtils.release(conn, st, rs);
87 }
88 }
89
90 /**
91 * @Method: testTransaction1
92 * @Description: 模拟转账过程中出现异常导致有一部分SQL执行失败时手动通知数据库回滚事务
94 *
95 */
96 @Test
97 public void testTransaction3(){
98 Connection conn = null;
99 PreparedStatement st = null;
100 ResultSet rs = null;
101
102 try{
103 conn = JdbcUtils.getConnection();
104 conn.setAutoCommit(false);//通知数据库开启事务(start transaction)
105 String sql1 = "update account set money=money-100 where name='A'";
106 st = conn.prepareStatement(sql1);
107 st.executeUpdate();
108 //用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交
109 int x = 1/0;
110 String sql2 = "update account set money=money+100 where name='B'";
111 st = conn.prepareStatement(sql2);
112 st.executeUpdate();
113 conn.commit();//上面的两条SQL执行Update语句成功之后就通知数据库提交事务(commit)
114 System.out.println("成功!!!");
115 }catch (Exception e) {
116 try {
117 //捕获到异常之后手动通知数据库执行回滚事务的操作
118 conn.rollback();
119 } catch (SQLException e1) {
120 e1.printStackTrace();
121 }
122 e.printStackTrace();
123 }finally{
124 JdbcUtils.release(conn, st, rs);
125 }
126 }
127 }
设置事务回滚点
在开发中,有时候可能需要手动设置事务的回滚点,在JDBC中使用如下的语句设置事务回滚点
Savepoint sp = conn.setSavepoint();
Conn.rollback(sp);
Conn.commit();//回滚后必须通知数据库提交事务
设置事务回滚点范例:
1 package me.gacl.demo;
2
3 import java.sql.Connection;
4 import java.sql.PreparedStatement;
5 import java.sql.ResultSet;
6 import java.sql.SQLException;
7 import java.sql.Savepoint;
8
9 import me.gacl.utils.JdbcUtils;
10 import org.junit.Test;
11
12 /**
13 * @ClassName: TransactionDemo1
14 * @Description:
15 * JDBC中使用事务来模似转帐
16 create table account(
17 id int primary key auto_increment,
18 name varchar(40),
19 money float
20 );
21 insert into account(name,money) values('A',1000);
22 insert into account(name,money) values('B',1000);
23 insert into account(name,money) values('C',1000);
26 *
27 */
28 public class TransactionDemo2 {
29
30 /**
31 * @Method: testTransaction1
32 * @Description: 模拟转账成功时的业务场景
34 *
35 */
36 @Test
37 public void testTransaction1(){
38 Connection conn = null;
39 PreparedStatement st = null;
40 ResultSet rs = null;
41 Savepoint sp = null;
42
43 try{
44 conn = JdbcUtils.getConnection();
45 conn.setAutoCommit(false);//通知数据库开启事务(start transaction)
46
47 String sql1 = "update account set money=money-100 where name='A'";
48 st = conn.prepareStatement(sql1);
49 st.executeUpdate();
50
51 //设置事务回滚点
52 sp = conn.setSavepoint();
53
54 String sql2 = "update account set money=money+100 where name='B'";
55 st = conn.prepareStatement(sql2);
56 st.executeUpdate();
57
58 //程序执行到这里出现异常,后面的sql3语句执行将会中断
59 int x = 1/0;
60
61 String sql3 = "update account set money=money+100 where name='C'";
62 st = conn.prepareStatement(sql3);
63 st.executeUpdate();
64
65 conn.commit();
66
67 }catch (Exception e) {
68 try {
69 /**
70 * 我们在上面向数据库发送了3条update语句,
71 * sql3语句由于程序出现异常导致无法正常执行,数据库事务而已无法正常提交,
72 * 由于设置的事务回滚点是在sql1语句正常执行完成之后,sql2语句正常执行之前,
73 * 那么通知数据库回滚事务时,不会回滚sql1执行的update操作
74 * 只会回滚到sql2执行的update操作,也就是说,上面的三条update语句中,sql1这条语句的修改操作起作用了
75 * sql2的修改操作由于事务回滚没有起作用,sql3由于程序异常没有机会执行
76 */
77 conn.rollback(sp);//回滚到设置的事务回滚点
78 //回滚了要记得通知数据库提交事务
79 conn.commit();
80 } catch (SQLException e1) {
81 e1.printStackTrace();
82 }
83 e.printStackTrace();
84 }finally{
85 JdbcUtils.release(conn, st, rs);
86 }
87 }
88 }
事务的四大特性(ACID)
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的SQL语句,要么全部执行成功,要么全部执行失败
一致性(Consistency)
官网上事务一致性的概念是:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。以转账为例子,A向B转账,假设转账之前这两个用户的钱加起来总共是2000,那么A向B转账之后,不管这两个账户怎么转,A用户的钱和B用户的钱加起来的总额还是2000,这个就是事务的一致性。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
事务的四大特性中最麻烦的是隔离性,下面重点介绍一下事务的隔离级别
事务的隔离级别
多个线程开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。
事务不考虑隔离性可能会引发的问题
如果事务不考虑隔离性,可能会引发如下问题:
1、脏读
脏读指一个事务读取了另外一个事务未提交的数据。
这是非常危险的,假设A向B转帐100元,对应sql语句如下所示
1.update account set money=money+100 where name='B';
2.update account set money=money-100 where name='A';
当第1条sql执行完,第2条还没执行(A未提交时),如果此时B查询自己的帐户,就会发现自己多了100元钱。如果A等B走后再回滚,B就会损失100元。
2、不可重复读
不可重复读指在一个事务内读取表中的某一行数据,多次读取结果不同。
例如银行想查询A帐户余额,第一次查询A帐户为200元,此时A向帐户内存了100元并提交了,银行接着又进行了一次查询,此时A帐户为300元了。银行两次查询不一致,可能就会很困惑,不知道哪次查询是准的。
不可重复读和脏读的区别是,脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据。
很多人认为这种情况就对了,无须困惑,当然是后面的为准。我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中,结果在一个事务中针对输出的目的地,进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
3、虚读(幻读)
虚读(幻读)是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
如丙存款100元未提交,这时银行做报表统计account表中所有用户的总额为500元,然后丙提交了,这时银行再统计发现帐户为600元了,造成虚读同样会使银行不知所措,到底以哪个为准。
事务隔离性的设置语句
MySQL数据库共定义了四种隔离级别:
- Serializable(串行化):可避免脏读、不可重复读、虚读情况的发生。
- Repeatable read(可重复读):可避免脏读、不可重复读情况的发生。
- Read committed(读已提交):可避免脏读情况发生。
- Read uncommitted(读未提交):最低级别,以上情况均无法保证。
mysql数据库查询当前事务隔离级别:select @@tx_isolation
例如:
mysql数据库默认的事务隔离级别是:Repeatable read(可重复读)
mysql数据库设置事务隔离级别:set transaction isolation level 隔离级别名
例如:
使用MySQL数据库演示不同隔离级别下的并发问题
同时打开两个窗口模拟2个用户并发访问数据库
1、当把事务的隔离级别设置为read uncommitted时,会引发脏读、不可重复读和虚读
A窗口
set transaction isolation level read uncommitted;--设置A用户的数据库隔离级别为Read uncommitted(读未提交)
start transaction;--开启事务
select * from account;--查询A账户中现有的钱,转到B窗口进行操作
select * from account--发现a多了100元,这时候A读到了B未提交的数据(脏读)
B窗口
start transaction;--开启事务
update account set money=money+100 where name='A';--不要提交,转到A窗口查询
2、当把事务的隔离级别设置为read committed时,会引发不可重复读和虚读,但避免了脏读
A窗口
set transaction isolation level read committed;
start transaction;
select * from account;--发现a帐户是1000元,转到b窗口
select * from account;--发现a帐户多了100,这时候,a读到了别的事务提交的数据,两次读取a帐户读到的是不同的结果(不可重复读)
B窗口
start transaction;
update account set money=money+100 where name='aaa';
commit;--转到a窗口
3、当把事务的隔离级别设置为repeatable read(mysql默认级别)时,会引发虚读,但避免了脏读、不可重复读
A窗口
set transaction isolation level repeatable read;
start transaction;
select * from account;--发现表有4个记录,转到b窗口
select * from account;--可能发现表有5条记录,这时候发生了a读取到另外一个事务插入的数据(虚读)
B窗口
start transaction;
insert into account(name,money) values('ggg',1000);
commit;--转到a窗口
4、当把事务的隔离级别设置为Serializable时,会避免所有问题
A窗口
set transaction isolation level Serializable;
start transaction;
select * from account;--转到b窗口
B窗口
start transaction;
insert into account(name,money) values('ggg',1000);--发现不能插入,只能等待a结束事务才能插入
数据库连接池
应用程序直接获取数据库连接的缺点
用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、拓机。如下图所示:
使用数据库连接池优化程序性能
数据库连接池的基本概念
数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标.数据库连接池正式针对这个问题提出来的.数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。如下图所示:
数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中, 这些数据库连接的数量是由最小数据库连接数来设定的.无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量.连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中.
数据库连接池的最小连接数和最大连接数的设置要考虑到以下几个因素:
- 最小连接数:是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费.
- 最大连接数:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作
- 如果最小连接数与最大连接数相差很大:那么最先连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接.不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,他将被放到连接池中等待重复使用或是空间超时后被释放.
编写数据库连接池
编写连接池需实现java.sql.DataSource接口。DataSource接口中定义了两个重载的getConnection方法:
- Connection getConnection()
- Connection getConnection(String username, String password)
实现DataSource接口,并实现连接池功能的步骤:
- 在DataSource构造函数中批量创建与数据库的连接,并把创建的连接加入LinkedList对象中。
- 实现getConnection方法,让getConnection方法每次调用时,从LinkedList中取一个Connection返回给用户。
当用户使用完Connection,调用Connection.close()方法时,Collection对象应保证将自己返回到LinkedList中,而不要把conn还给数据库。Collection保证将自己返回到LinkedList中是此处编程的难点。
数据库连接池核心代码
使用动态代理技术构建连接池中的connection
proxyConn = (Connection) Proxy.newProxyInstance(this.getClass()
.getClassLoader(), conn.getClass().getInterfaces(),
new InvocationHandler() {
//此处为内部类,当close方法被调用时将conn还回池中,其它方法直接执行
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
if (method.getName().equals("close")) {
pool.addLast(conn);
return null;
}
return method.invoke(conn, args);
}
});
数据库连接池编写范例:
1 package me.gacl.demo;
2
3 import java.io.InputStream;
4 import java.io.PrintWriter;
5 import java.lang.reflect.InvocationHandler;
6 import java.lang.reflect.Method;
7 import java.lang.reflect.Proxy;
8 import java.sql.Connection;
9 import java.sql.DriverManager;
10 import java.sql.SQLException;
11 import java.util.LinkedList;
12 import java.util.Properties;
13 import javax.sql.DataSource;
14
15 /**
16 * @ClassName: JdbcPool
17 * @Description:编写数据库连接池
20 *
21 */
22 public class JdbcPool implements DataSource{
23
24 /**
25 * @Field: listConnections
26 * 使用LinkedList集合来存放数据库链接,
27 * 由于要频繁读写List集合,所以这里使用LinkedList存储数据库连接比较合适
28 */
29 private static LinkedList<Connection> listConnections = new LinkedList<Connection>();
30
31 static{
32 //在静态代码块中加载db.properties数据库配置文件
33 InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("db.properties");
34 Properties prop = new Properties();
35 try {
36 prop.load(in);
37 String driver = prop.getProperty("driver");
38 String url = prop.getProperty("url");
39 String username = prop.getProperty("username");
40 String password = prop.getProperty("password");
41 //数据库连接池的初始化连接数大小
42 int jdbcPoolInitSize =Integer.parseInt(prop.getProperty("jdbcPoolInitSize"));
43 //加载数据库驱动
44 Class.forName(driver);
45 for (int i = 0; i < jdbcPoolInitSize; i++) {
46 Connection conn = DriverManager.getConnection(url, username, password);
47 System.out.println("获取到了链接" + conn);
48 //将获取到的数据库连接加入到listConnections集合中,listConnections集合此时就是一个存放了数据库连接的连接池
49 listConnections.add(conn);
50 }
51
52 } catch (Exception e) {
53 throw new ExceptionInInitializerError(e);
54 }
55 }
56
57 @Override
58 public PrintWriter getLogWriter() throws SQLException {
59 // TODO Auto-generated method stub
60 return null;
61 }
62
63 @Override
64 public void setLogWriter(PrintWriter out) throws SQLException {
65 // TODO Auto-generated method stub
66
67 }
68
69 @Override
70 public void setLoginTimeout(int seconds) throws SQLException {
71 // TODO Auto-generated method stub
72
73 }
74
75 @Override
76 public int getLoginTimeout() throws SQLException {
77 // TODO Auto-generated method stub
78 return 0;
79 }
80
81 @Override
82 public <T> T unwrap(Class<T> iface) throws SQLException {
83 // TODO Auto-generated method stub
84 return null;
85 }
86
87 @Override
88 public boolean isWrapperFor(Class<?> iface) throws SQLException {
89 // TODO Auto-generated method stub
90 return false;
91 }
92
93 /* 获取数据库连接
94 * @see javax.sql.DataSource#getConnection()
95 */
96 @Override
97 public Connection getConnection() throws SQLException {
98 //如果数据库连接池中的连接对象的个数大于0
99 if (listConnections.size()>0) {
100 //从listConnections集合中获取一个数据库连接
101 final Connection conn = listConnections.removeFirst();
102 System.out.println("listConnections数据库连接池大小是" + listConnections.size());
103 //返回Connection对象的代理对象
104 return (Connection) Proxy.newProxyInstance(JdbcPool.class.getClassLoader(), conn.getClass().getInterfaces(), new InvocationHandler(){
105 @Override
106 public Object invoke(Object proxy, Method method, Object[] args)
107 throws Throwable {
108 if(!method.getName().equals("close")){
109 return method.invoke(conn, args);
110 }else{
111 //如果调用的是Connection对象的close方法,就把conn还给数据库连接池
112 listConnections.add(conn);
113 System.out.println(conn + "被还给listConnections数据库连接池了!!");
114 System.out.println("listConnections数据库连接池大小为" + listConnections.size());
115 return null;
116 }
117 }
118 });
119 }else {
120 throw new RuntimeException("对不起,数据库忙");
121 }
122 }
123
124 @Override
125 public Connection getConnection(String username, String password)
126 throws SQLException {
127 return null;
128 }
129 }
db.properties配置文件如下:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbcStudy
username=root
password=XDP
jdbcPoolInitSize=10
写一个JdbcUtil测试数据库连接池
1 package me.gacl.utils;
2
3 import java.sql.Connection;
4 import java.sql.ResultSet;
5 import java.sql.SQLException;
6 import java.sql.Statement;
7 import me.gacl.demo.JdbcPool;
8
9 public class JdbcUtil {
10
11 /**
12 * @Field: pool
13 * 数据库连接池
14 */
15 private static JdbcPool pool = new JdbcPool();
16
17 /**
18 * @Method: getConnection
19 * @Description: 从数据库连接池中获取数据库连接对象
21 * @return Connection数据库连接对象
22 * @throws SQLException
23 */
24 public static Connection getConnection() throws SQLException{
25 return pool.getConnection();
26 }
27
28 /**
29 * @Method: release
30 * @Description: 释放资源,
31 * 释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象
34 * @param conn
35 * @param st
36 * @param rs
37 */
38 public static void release(Connection conn,Statement st,ResultSet rs){
39 if(rs!=null){
40 try{
41 //关闭存储查询结果的ResultSet对象
42 rs.close();
43 }catch (Exception e) {
44 e.printStackTrace();
45 }
46 rs = null;
47 }
48 if(st!=null){
49 try{
50 //关闭负责执行SQL命令的Statement对象
51 st.close();
52 }catch (Exception e) {
53 e.printStackTrace();
54 }
55 }
56
57 if(conn!=null){
58 try{
59 //关闭Connection数据库连接对象
60 conn.close();
61 }catch (Exception e) {
62 e.printStackTrace();
63 }
64 }
65 }
66 }
开源数据库连接池
现在很多WEB服务器(Weblogic, WebSphere, Tomcat)都提供了DataSoruce的实现,即连接池的实现。通常我们把DataSource的实现,按其英文含义称之为数据源,数据源中都包含了数据库连接池的实现。
也有一些开源组织提供了数据源的独立实现:
- DBCP 数据库连接池
- C3P0 数据库连接池
在使用了数据库连接池之后,在项目的实际开发中就不需要编写连接数据库的代码了,直接从数据源获得数据库的连接。
DBCP数据源
DBCP 是 Apache 软件基金组织下的开源连接池实现,要使用DBCP数据源,需要应用程序应在系统中增加如下两个 jar 文件:
- Commons-dbcp.jar:连接池的实现
- Commons-pool.jar:连接池实现的依赖库
Tomcat 的连接池正是采用该连接池来实现的。该数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。
在应用程序中加入dbcp连接池
1.导入相关jar包
commons-dbcp-1.2.2.jar、commons-pool.jar
2、在类目录下加入dbcp的配置文件:dbcpconfig.properties
dbcpconfig.properties的配置信息如下:
#连接设置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbcstudy
username=root
password=XDP
#<!-- 初始化连接 -->
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
如下图所示:
3、在获取数据库连接的工具类(如jdbcUtils)的静态代码块中创建池
1 package me.gacl.util;
2
3 import java.io.InputStream;
4 import java.sql.Connection;
5 import java.sql.ResultSet;
6 import java.sql.SQLException;
7 import java.sql.Statement;
8 import java.util.Properties;
9 import javax.sql.DataSource;
10 import org.apache.commons.dbcp.BasicDataSourceFactory;
11
12 /**
13 * @ClassName: JdbcUtils_DBCP
14 * @Description: 数据库连接工具类
17 *
18 */
19 public class JdbcUtils_DBCP {
20 /**
21 * 在java中,编写数据库连接池需实现java.sql.DataSource接口,每一种数据库连接池都是DataSource接口的实现
22 * DBCP连接池就是java.sql.DataSource接口的一个具体实现
23 */
24 private static DataSource ds = null;
25 //在静态代码块中创建数据库连接池
26 static{
27 try{
28 //加载dbcpconfig.properties配置文件
29 InputStream in = JdbcUtils_DBCP.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
30 Properties prop = new Properties();
31 prop.load(in);
32 //创建数据源
33 ds = BasicDataSourceFactory.createDataSource(prop);
34 }catch (Exception e) {
35 throw new ExceptionInInitializerError(e);
36 }
37 }
38
39 /**
40 * @Method: getConnection
41 * @Description: 从数据源中获取数据库连接
43 * @return Connection
44 * @throws SQLException
45 */
46 public static Connection getConnection() throws SQLException{
47 //从数据源中获取数据库连接
48 return ds.getConnection();
49 }
50
51 /**
52 * @Method: release
53 * @Description: 释放资源,
54 * 释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象
56 *
57 * @param conn
58 * @param st
59 * @param rs
60 */
61 public static void release(Connection conn,Statement st,ResultSet rs){
62 if(rs!=null){
63 try{
64 //关闭存储查询结果的ResultSet对象
65 rs.close();
66 }catch (Exception e) {
67 e.printStackTrace();
68 }
69 rs = null;
70 }
71 if(st!=null){
72 try{
73 //关闭负责执行SQL命令的Statement对象
74 st.close();
75 }catch (Exception e) {
76 e.printStackTrace();
77 }
78 }
79
80 if(conn!=null){
81 try{
82 //将Connection连接对象还给数据库连接池
83 conn.close();
84 }catch (Exception e) {
85 e.printStackTrace();
86 }
87 }
88 }
89 }
测试DBCP数据源
package me.gacl.test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import org.junit.Test;
import me.gacl.util.JdbcUtils_DBCP;
public class DataSourceTest {
@Test
public void dbcpDataSourceTest() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try{
//获取数据库连接
conn = JdbcUtils_DBCP.getConnection();
String sql = "insert into test1(name) values(?)";
st = conn.prepareStatement(sql);
st.setString(1, "gacl");
st.executeUpdate();
//获取数据库自动生成的主键
rs = st.getGeneratedKeys();
if(rs.next()){
System.out.println(rs.getInt(1));
}
}catch (Exception e) {
e.printStackTrace();
}finally{
//释放资源
JdbcUtils_DBCP.release(conn, st, rs);
}
}
}
C3P0数据源
C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。C3P0数据源在项目开发中使用得比较多。
c3p0与dbcp区别
- dbcp没有自动回收空闲连接的功能
- c3p0有自动回收空闲连接功能
在应用程序中加入C3P0连接池
1.导入相关jar包
c3p0-0.9.2-pre1.jar、mchange-commons-0.2.jar,如果操作的是Oracle数据库,那么还需要导入c3p0-oracle-thin-extras-0.9.2-pre1.jar
2、在类目录下加入C3P0的配置文件:c3p0-config.xml
c3p0-config.xml的配置信息如下:
<?xml version="1.0" encoding="UTF-8"?>
<!--
c3p0-config.xml必须位于类路径下面
private static ComboPooledDataSource ds;
static{
try {
ds = new ComboPooledDataSource("MySQL");
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
-->
<c3p0-config>
<!--
C3P0的缺省(默认)配置,
如果在代码中“ComboPooledDataSource ds = new ComboPooledDataSource();”这样写就表示使用的是C3P0的缺省(默认)配置信息来创建数据源
-->
<default-config>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbcstudy</property>
<property name="user">root</property>
<property name="password">XDP</property>
<property name="acquireIncrement">5</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">5</property>
<property name="maxPoolSize">20</property>
</default-config>
<!--
C3P0的命名配置,
如果在代码中“ComboPooledDataSource ds = new ComboPooledDataSource("MySQL");”这样写就表示使用的是name是MySQL的配置信息来创建数据源
-->
<named-config name="MySQL">
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbcstudy</property>
<property name="user">root</property>
<property name="password">XDP</property>
<property name="acquireIncrement">5</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">5</property>
<property name="maxPoolSize">20</property>
</named-config>
</c3p0-config>
如下图所示:
3、在获取数据库连接的工具类(如jdbcUtils)的静态代码块中创建池
1 package me.gacl.util;
2
3 import java.sql.Connection;
4 import java.sql.ResultSet;
5 import java.sql.SQLException;
6 import java.sql.Statement;
7 import com.mchange.v2.c3p0.ComboPooledDataSource;
8
9 /**
10 * @ClassName: JdbcUtils_C3P0
11 * @Description: 数据库连接工具类
15 */
16 public class JdbcUtils_C3P0 {
17
18 private static ComboPooledDataSource ds = null;
19 //在静态代码块中创建数据库连接池
20 static{
21 try{
22 //通过代码创建C3P0数据库连接池
23 /*ds = new ComboPooledDataSource();
24 ds.setDriverClass("com.mysql.jdbc.Driver");
25 ds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbcstudy");
26 ds.setUser("root");
27 ds.setPassword("XDP");
28 ds.setInitialPoolSize(10);
29 ds.setMinPoolSize(5);
30 ds.setMaxPoolSize(20);*/
31
32 //通过读取C3P0的xml配置文件创建数据源,C3P0的xml配置文件c3p0-config.xml必须放在src目录下
33 //ds = new ComboPooledDataSource();//使用C3P0的默认配置来创建数据源
34 ds = new ComboPooledDataSource("MySQL");//使用C3P0的命名配置来创建数据源
35
36 }catch (Exception e) {
37 throw new ExceptionInInitializerError(e);
38 }
39 }
40
41 /**
42 * @Method: getConnection
43 * @Description: 从数据源中获取数据库连接
45 * @return Connection
46 * @throws SQLException
47 */
48 public static Connection getConnection() throws SQLException{
49 //从数据源中获取数据库连接
50 return ds.getConnection();
51 }
52
53 /**
54 * @Method: release
55 * @Description: 释放资源,
56 * 释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象
58 *
59 * @param conn
60 * @param st
61 * @param rs
62 */
63 public static void release(Connection conn,Statement st,ResultSet rs){
64 if(rs!=null){
65 try{
66 //关闭存储查询结果的ResultSet对象
67 rs.close();
68 }catch (Exception e) {
69 e.printStackTrace();
70 }
71 rs = null;
72 }
73 if(st!=null){
74 try{
75 //关闭负责执行SQL命令的Statement对象
76 st.close();
77 }catch (Exception e) {
78 e.printStackTrace();
79 }
80 }
81
82 if(conn!=null){
83 try{
84 //将Connection连接对象还给数据库连接池
85 conn.close();
86 }catch (Exception e) {
87 e.printStackTrace();
88 }
89 }
90 }
91 }
测试C3P0数据源
package me.gacl.test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import org.junit.Test;
import me.gacl.util.JdbcUtils_C3P0;
import me.gacl.util.JdbcUtils_DBCP;
public class DataSourceTest {
@Test
public void c3p0DataSourceTest() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try{
//获取数据库连接
conn = JdbcUtils_C3P0.getConnection();
String sql = "insert into test1(name) values(?)";
st = conn.prepareStatement(sql);
st.setString(1, "gacl"); st.executeUpdate();
//获取数据库自动生成的主键
rs = st.getGeneratedKeys();
if(rs.next()){
System.out.println(rs.getInt(1));
}
}catch (Exception e) {
e.printStackTrace();
}finally{
//释放资源
JdbcUtils_C3P0.release(conn, st, rs);
}
}
}
配置Tomcat数据源
在实际开发中,我们有时候还会使用服务器提供给我们的数据库连接池,比如我们希望Tomcat服务器在启动的时候可以帮我们创建一个数据库连接池,那么我们在应用程序中就不需要手动去创建数据库连接池,直接使用Tomcat服务器创建好的数据库连接池即可。要想让Tomcat服务器在启动的时候帮我们创建一个数据库连接池,那么需要简单配置一下Tomcat服务器。
JNDI技术简介
JNDI(Java Naming and Directory Interface),Java命名和目录接口,它对应于J2SE中的javax.naming包,
这 套API的主要作用在于:它可以把Java对象放在一个容器中(JNDI容器),并为容器中的java对象取一个名称,以后程序想获得Java对象,只需 通过名称检索即可。其核心API为Context,它代表JNDI容器,其lookup方法为检索容器中对应名称的对象。
Tomcat服务器创建的数据源是以JNDI资源的形式发布的,所以说在Tomat服务器中配置一个数据源实际上就是在配置一个JNDI资源,通过查看Tomcat文档,我们知道使用如下的方式配置tomcat服务器的数据源:
<Context>
<Resource name="jdbc/datasource" auth="Container"
type="javax.sql.DataSource" username="root" password="XDP"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/jdbcstudy"
maxActive="8" maxIdle="4"/>
</Context>
服务器创建好数据源之后,我们的应用程序又该怎么样得到这个数据源呢,Tomcat服务器创建好数据源之后是以JNDI的形式绑定到一个JNDI容器中的,我们可以把JNDI想象成一个大大的容器,我们可以往这个容器中存放一些对象,一些资源,JNDI容器中存放的对象和资源都会有一个独一无二的名称,应用程序想从JNDI容器中获取资源时,只需要告诉JNDI容器要获取的资源的名称,JNDI根据名称去找到对应的资源后返回给应用程序。我们平时做javaEE开发时,服务器会为我们的应用程序创建很多资源,比如request对象,response对象,服务器创建的这些资源有两种方式提供给我们的应用程序使用:第一种是通过方法参数的形式传递进来,比如我们在Servlet中写的doPost和doGet方法中使用到的request对象和response对象就是服务器以参数的形式传递给我们的。第二种就是JNDI的方式,服务器把创建好的资源绑定到JNDI容器中去,应用程序想要使用资源时,就直接从JNDI容器中获取相应的资源即可。
对于上面的name="jdbc/datasource"数据源资源,在应用程序中可以用如下的代码去获取
Context initCtx = new InitialContext();
Context envCtx = (Context) initCtx.lookup("java:comp/env");
dataSource = (DataSource)envCtx.lookup("jdbc/datasource");
此种配置下,数据库的驱动jar文件需放置在tomcat的lib下
配置Tomcat数据源
1、在Web项目的WebRoot目录下的META-INF目录创建一个context.xml文件
如下图所示:
2、在context.xml文件配置tomcat服务器的数据源
<Context>
<Resource
name="jdbc/datasource"
auth="Container"
type="javax.sql.DataSource"
username="root"
password="XDP"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/jdbcstudy"
maxActive="8"
maxIdle="4"/>
</Context>
3、将数据库的驱动jar文件需放置在tomcat的lib下
4、在获取数据库连接的工具类(如jdbcUtils)的静态代码块中获取JNDI容器中的数据源
1 package me.gacl.util;
2
3 import java.sql.Connection;
4 import java.sql.ResultSet;
5 import java.sql.SQLException;
6 import java.sql.Statement;
7 import javax.naming.Context;
8 import javax.naming.InitialContext;
9 import javax.sql.DataSource;
10
11 /**
12 * @ClassName: JdbcUtils_DBCP
13 * @Description: 数据库连接工具类
16 *
17 */
18 public class JdbcUtils_JNDI {
19
20 private static DataSource ds = null;
21 //在静态代码块中创建数据库连接池
22 static{
23 try{
24 //初始化JNDI
25 Context initCtx = new InitialContext();
26 //得到JNDI容器
27 Context envCtx = (Context) initCtx.lookup("java:comp/env");
28 //从JNDI容器中检索name为jdbc/datasource的数据源
29 ds = (DataSource)envCtx.lookup("jdbc/datasource");
30 }catch (Exception e) {
31 throw new ExceptionInInitializerError(e);
32 }
33 }
34
35 /**
36 * @Method: getConnection
37 * @Description: 从数据源中获取数据库连接
39 * @return Connection
40 * @throws SQLException
41 */
42 public static Connection getConnection() throws SQLException{
43 //从数据源中获取数据库连接
44 return ds.getConnection();
45 }
46
47 /**
48 * @Method: release
49 * @Description: 释放资源,
50 * 释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象
52 *
53 * @param conn
54 * @param st
55 * @param rs
56 */
57 public static void release(Connection conn,Statement st,ResultSet rs){
58 if(rs!=null){
59 try{
60 //关闭存储查询结果的ResultSet对象
61 rs.close();
62 }catch (Exception e) {
63 e.printStackTrace();
64 }
65 rs = null;
66 }
67 if(st!=null){
68 try{
69 //关闭负责执行SQL命令的Statement对象
70 st.close();
71 }catch (Exception e) {
72 e.printStackTrace();
73 }
74 }
75
76 if(conn!=null){
77 try{
78 //将Connection连接对象还给数据库连接池
79 conn.close();
80 }catch (Exception e) {
81 e.printStackTrace();
82 }
83 }
84 }
85 }
写一个Servlet测试JNDI数据源
package me.gacl.test;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import me.gacl.util.JdbcUtils_JNDI;
public class JNDITest extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Connection conn = null; PreparedStatement st = null;
ResultSet rs = null;
try{
//获取数据库连接
conn = JdbcUtils_JNDI.getConnection(); String sql = "insert into test1(name) values(?)";
st = conn.prepareStatement(sql);
st.setString(1, "gacl");
st.executeUpdate();
//获取数据库自动生成的主键
rs = st.getGeneratedKeys();
if(rs.next()){
System.out.println(rs.getInt(1));
}
}catch (Exception e) {
e.printStackTrace();
}finally{
//释放资源
JdbcUtils_JNDI.release(conn, st, rs);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}