JDBC二部曲之_事物、连接池

时间:2024-01-21 22:43:51

事务

事务概述

事务的四大特性(ACID)

事务的四大特性是:

l  原子性(Atomicity):事务中所有操作是不可再分割的原子单位。事务中所有操作要么全部执行成功,要么全部执行失败。

l  一致性(Consistency):事务执行后,数据库状态与其它业务规则保持一致。如转账业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。

l  隔离性(Isolation):隔离性是指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。

l  持久性(Durability):一旦事务提交成功,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据。

 JDBC中的事务

Connection的三个方法与事务相关:

l  setAutoCommit(boolean):设置是否为自动提交事务,如果true(默认值就是true)表示自动提交,也就是每条执行的SQL语句都是一个单独的事务,如果设置false,那么就相当于开启了事务了;

l  commit():提交结束事务;

l  rollback():回滚结束事务。

public void transfer(boolean b) {

Connection con = null;

PreparedStatement pstmt = null;

try {

con = JdbcUtils.getConnection();

//手动提交

con.setAutoCommit(false);// 设置为手动提交事务,即开启了事务。

String sql = "update account set balance=balance+? where   id=?";

pstmt = con.prepareStatement(sql);

//操作

pstmt.setDouble(1, -10000);

pstmt.setInt(2, 1);

pstmt.executeUpdate();

// 在两个操作中抛出异常

if(b) {

throw new Exception();//如果出现了异常就回滚结束事务

}

pstmt.setDouble(1, 10000);

pstmt.setInt(2, 2);

pstmt.executeUpdate();

//提交事务

con.commit();//当两个操作都执行完了,提交结束事务。

} catch(Exception e) {

//回滚事务

if(con != null) {

try {

con.rollback();//当出现异常时,回滚事务

} catch(SQLException ex) {}

}

throw new RuntimeException(e);

} finally {

//关闭

JdbcUtils.close(con, pstmt);

}

}

保存点

保存点是JDBC3.0的东西!当要求数据库服务器支持保存点方式的回滚。

校验数据库服务器是否支持保存点!

boolean b =   con.getMetaData().supportsSavepoints();

保存点的作用是允许事务回滚到指定的保存点位置。在事务中设置好保存点,然后回滚时可以选择回滚到指定的保存点,而不是回滚整个事务!注意,回滚到指定保存点并没有结束事务!!!只有回滚了整个事务才算是结束事务了!

Connection类的设置保存点,以及回滚到指定保存点方法:

l  设置保存点:Savepoint setSavepoint();

l  回滚到指定保存点:void rollback(Savepoint)。

/*

* 李四对张三说,如果你给我转1W,我就给你转100W。

*   ==========================================

*

* 张三给李四转1W(张三减去1W,李四加上1W)

* 设置保存点!

* 李四给张三转100W(李四减去100W,张三加上100W)

* 查看李四余额为负数,那么回滚到保存点。

* 提交事务

*/

@Test

public void fun() {

Connection con = null;

PreparedStatement pstmt = null;

try {

con = JdbcUtils.getConnection();

//手动提交

con.setAutoCommit(false);

String sql = "update account set balance=balance+? where   name=?";

pstmt = con.prepareStatement(sql);

//操作1(张三减去1W)

pstmt.setDouble(1, -10000);

pstmt.setString(2, "zs");

pstmt.executeUpdate();

//操作2(李四加上1W)

pstmt.setDouble(1, 10000);

pstmt.setString(2, "ls");

pstmt.executeUpdate();

// 设置保存点

Savepoint sp = con.setSavepoint();

//操作3(李四减去100W)

pstmt.setDouble(1, -1000000);

pstmt.setString(2, "ls");

pstmt.executeUpdate();

//操作4(张三加上100W)

pstmt.setDouble(1, 1000000);

pstmt.setString(2, "zs");

pstmt.executeUpdate();

//操作5(查看李四余额)

sql = "select balance from account where name=?";

pstmt = con.prepareStatement(sql);

pstmt.setString(1, "ls");

ResultSet rs =   pstmt.executeQuery();

rs.next();

double balance = rs.getDouble(1);

      //如果李四余额为负数,那么回滚到指定保存点

if(balance < 0) {

con.rollback(sp);

System.out.println("张三,你上当了!");

}

//提交事务

con.commit();//注意,一定要提交事务,因为回滚到指定保存点不会结束事务!保存点之前的操作没有被回滚,只能提交了才能真正把没有回滚的操作执行了。

} catch(Exception e) {

//回滚事务

if(con != null) {

try {

con.rollback();

} catch(SQLException ex) {}

}

throw new RuntimeException(e);

} finally {

//关闭

JdbcUtils.close(con, pstmt);

}

}

事务隔离级别

1 四大隔离级别

  事务隔离级别是用来处理与事务并发相关的问题!你可以想象一下,两个人同时对同一个银行账户进行操作是什么结果。

隔离级别

脏读

不可重复读

虚读

第一类丢失更新

第二类丢失更新

READ UNCOMMITTED

允许

允许

允许

不允许

允许

READ COMMITTED

不允许

允许

允许

不允许

允许

REPEATABLE READ

不允许

不允许

允许

不允许

不允许

SERIALIZABLE

不允许

不允许

不允许

不允许

不允许

2 五大并发事务问题

因为并发事务导致的问题大致有5类,其中两类是更新问题,三类是读问题。

l  脏读(dirty read):读到未提交更新数据

时间

转账事务A

取款事务B

T1

开始事务

T2

开始事务

T3

查询账户余额为1000元

T4

取出500元把余额改为500元

T5

查看账户余额为500元(脏读)

T6

撤销事务,余额恢复为1000元

T7

汇入100元把余额改为600元

T8

提交事务

  

A事务查询到了B事务未提交的更新数据,A事务依据这个查询结果继续执行相关操作。但是接着B事务撤销了所做的更新,这会导致A事务操作的是脏数据。(这是绝对不允许出现的事情)

l  虚读(phantom read):读到已提交插入数据

时间

统计金额事务A

转账事务B

T1

开始事务

T2

开始事务

T3

统计总存款数为10000元

T4

新增一个存款账户,存款为100元

T5

提交事务

T6

再次统计总存款数为10100元

A事务第一次查询时,没有问题,第二次查询时查到了B事务已提交的新插入数据,这导致两次查询结果不同。(在实际开发中,很少会对相同数据进行两次查询,所以可以考虑是否允许虚读)

l  不可重复读(unrepeatable read):读到已提交更新数据

时间

取款事务A

转账事务B

T1

开始事务

T2

开始事务

T3

查询账户余额为1000元

T4

查询账户余额为1000元

T5

取出100元,把余额改为900元

T6

提交事务

T7

查询账户余额为900元(与T4读取的一不一致)

不可重复读与虚读有些相似,都是两次查询的结果不同。后者是查询到了另一个事务已提交的新插入数据,而前者是查询到了另一个事务已提交的更新数据。

l  第一类丢失更新:撤销了已提交数据

时间

取款事务A

转账事务B

T1

开始事务

 

T2

 

开始事务

T3

查询账户余额为1000元

T4

查询账户余额为1000元

T5

汇入100元把余额改为1100元

T6

提交事务

T7

取出100元把余额改为900元

T8

撤销事务

T9

余额恢复为1000元

这种并发问题是由于完全没有隔离事务造成的。当两个事务更新相同的数据时,如果一个事务被提交,另一个事务却撤销,那么会连同第一个事务所做的更新也被撤销了。(这是绝对不允许出现的事情)

l  第二类丢失更新:提交覆盖已提交事务

时间

取款事务A

转账事务B

T1

开始事务

T2

开始事务

T3

查询账户余额为1000元

T4

查询账户余额为1000元

T5

取出100元把余额改为900元

T6

提交事务

T7

汇入100元

T8

提交事务

T9

余额恢复为1100元

这是在实际应用中经常遇到的并发问题,它和不可重复读本质上是同一类并发问题,通常把它看做是不可重复读的一个特例。两个或多个事务查询同一数据。然后都基于自己的查询结果更新数据,这时会造成最后一个提交的更新事务,将覆盖其它已经提交的更新事务。

3 哪种隔离级别最好

4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题的能力是不同的。

1 SERIALIZABLE(串行化)

当数据库系统使用SERIALIZABLE隔离级别时,一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。

2 REPEATABLE READ(可重复读)

当数据库系统使用REPEATABLE READ隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他事务对已有记录的更新。

3 READ COMMITTED(读已提交数据)

  当数据库系统使用READ COMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且还能看到其他事务已经提交的对已有记录的更新。

4 READ UNCOMMITTED(读未提交数据)

  当数据库系统使用READ UNCOMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且还能看到其他事务没有提交的对已有记录的更新。

你可能会说,选择SERIALIZABLE,因为它最安全!没错,它是最安全,但它也是最慢的!四种隔离级别的安全性与性能成反比!最安全的性能最差,最不安全的性能最好!

MySQL的默认隔离级别为REPEATABLE READ,这是一个很不错的选择吧!

4 MySQL隔离级别

MySQL的默认隔离级别为Repeatable read,可以通过下面语句查看:

seelect @@session.tx_isolation

seelect @@global.tx_isolation

也可以通过下面语句来设置当前连接的隔离级别:

set global transaction isolationlevel [4先1]

set session transaction isolationlevel [4先1]

4 JDBC设置隔离级别

con. setTransactionIsolation(int level)

参数可选值如下:

l  Connection.TRANSACTION_READ_UNCOMMITTED;

l  Connection.TRANSACTION_READ_COMMITTED;

l  Connection.TRANSACTION_REPEATABLE_READ;

l  Connection.TRANSACTION_SERIALIZABLE。

数据库连接池

数据库连接池概念

1 什么是数据库连接池

池?对象池也!数据库连接池?池中存放的对象是数据库连接对象,即Connection。

池的作用不外乎就是管理对象的生命周期!也就是说,有了池,你想要对象时,向池要对象就可以了,而不是自己来创建了!对象用完了,你也不要去销毁它,而是把对象回给池。

Connection的创建与销毁开销都很大,所以我们需要使用池来管理它。

2 JDBC数据库连接池接口(DataSource)

当定义了数据库连接池之后,程序需要使用Connection都是从连接池那来获取,但是怎么来获取连接池对象呢?这里又存在一个解耦的问题!

通常在Java Web环境中都会使用JNDI(Java命名目录接口)来查找想要的资源,通常JavaWeb服务器也都是JNDI服务器,所以使用JNDI需要在JavaWeb服务器(Tomcat)中进行配置。

现在先不用去管什么是JNDI,但你要记住的是,如果需要你的连接池可以配置到JNDI中,那么就要让你的连接池去实现DataSource接口。

3 自定义连接池ItcastDataSource

其实写一个简单的连接池很容易,不过就是写一个类,这类中有一个List<Connection>属性,先创建几个Connection对象,放到List中,当用户调用getConnection()方法时,直接从List中获取已经创建好的Connection对象即可。当用户使用完了Connection对象后,再把Connectoin还给池就可以了。

一.先试一试第一种方式

实现DataSource规范

public class MyDataSource2 implements   DataSource {

public   static List<Connection> pool = new   ArrayList<Connection>();// 池子

private   static int   size = 10;

public   static void   setSize(int size) {

MyDataSource2.size   = size;

}

/**

* 存在必要性?

*          数据库连接作为非常重要的资源 ,用完了要不要立即销毁?----(不能立即关闭)

* 不能立即关闭?谁维护

*                 ---------------专门交给一个人来管理(池-----------池子中放了好多数据库连接)

*

* 非常 有必要存在!!!

*                 池子在java类中如何表现?--------------------集合--------(Connection)----用什么集合?

*

* List    集合来模拟池

*                 1.初始化一些连接

*

* @author wangli

*

*/

// 初始化池中的连接

static   {

try   {

for (int   i = 0; i < size; i++) {

pool.add(JdbcUtil.getConnection());

}

} catch   (Exception e) {

e.printStackTrace();

}

}

// 统一提供方法,用于从池中获取连接

public   synchronized Connection   getConnection() {

if   (pool.size() > 0) {

Connection con = pool.remove(0);//   删除这个连接,因为别人不能此时不能再用它

MyConnection1 con2 = new MyConnection1(con, pool);//

return con2;

} else   {

throw new   RuntimeException("池中无连接");

}

}

。。。。。。。。

}

ConnectionAdapter 实现Connection   可以使我们直接继承这个类

public class ConnectionAdapter implements Connection {

// 适配器模式

一个中转接口   MyConnection1  类可以继承而不用实现它

......

}

MyConnection1类  继承ConnectionAdapter类

//包装模式                                     //似你

public class MyConnection1  extends ConnectionAdapter {

private Connection con;//还有你

private List<Connection> pool;

//拜托你

public MyConnection1(Connection   con,List<Connection> pool){

this.con    = con;

this.pool = pool;

}

@Override    //改写它

public void close() throws SQLException {

pool.add(con);

}

}

测试类

 

 

public class   MyDataSource1Test3 {

/**

* @param args

*/

public static void main(String[] args)   {

DataSource myds = new   MyDataSource2();

//System.out.println("初始化好了,池中连接数:"+myds.pool.size());

try {

Connection con =   myds.getConnection();//从池中取一个连接

//System.out.println("取出但未还,池中连接数:"+myds.pool.size());

System.out.println("当前连接对象:"+con);

Statement st = con.createStatement();//问题是st为null,原因是MyConnection对于这个方法只是返回null

System.out.println(st);

con.close();

} catch (SQLException e) {

e.printStackTrace();

}

//System.out.println("用完后还了,池中连接数:"+myds.pool.size());

}

}

 

这种方法继承原有Connection类,如果不重写Connection。那么会导致其他方法不能使用,如:Statement st = con.createStatement();st=null,如果写,那就代码量太多,耦合度太高!将来你写的连接池只能应用在MySQL的某个版本的驱动上了。行不通!

 

第二种方法 动态代理

public class MyDataSource3 implements DataSource {

public   static List<Connection> pool = new   ArrayList<Connection>();// 创建链接池

private   static int   size = 10;

public   static void   setSize(int size) {

MyDataSource3.size = size;

}

// 初始化池中的连接

static   {

try   {

for (int   i = 0; i < size; i++) {

pool.add(JdbcUtil.getConnection());

}

} catch   (Exception e) {

e.printStackTrace();

}

}

// 统一提供方法,用于从池中获取连接

public   synchronized Connection   getConnection() {

if   (pool.size() > 0) {

final Connection con = pool.remove(0);//   删除这个连接,因为别人不能此时不能再用它

//   代理模式 JDK自带的相应代理相关类

//   Proxy动态代理相关类

//   Proxy

//   newProxyInstance(ClassLoader cl,Class<?>[]

//   interfaces,InvocationHandler ih)

//   ClassLoader :代表类加载器 被代理的对象用什么类加载器,代理对象就要用什么加载器

//   interfaces :代表所实现的接口,被代理对象实现什么接口,代理对象就要实现什么接口

//   InvocationHandler 处理器 就是要处理相关的緢节问题 一般有一个匿名内部类

//   InvocationHandler 策略模式

Connection proxyConn =   (Connection) Proxy.newProxyInstance(con

.getClass().getClassLoader(),   con.getClass()

.getInterfaces(),   new InvocationHandler() {

public Object invoke(Object proxy, Method   method, Object[] args)

throws Throwable {

if ("close".equals(method.getName()))   {

// 对于close方法要改写 添加到池中

return pool.add(con);// 加入池中

} else {

// 对于其它方法,用mysql的connection对象的底层实现

return method.invoke(con, args);

}

}

});

return proxyConn;

} else   {

throw new   RuntimeException("池中无连接");

}

}

测试类

public   class DataSource1Test4 {

/**

* @param args

*/

public static void main(String[] args)   {

MyDataSource3 myds = new   MyDataSource3();

System.out.println("初始化好了,池中连接数:"+myds.pool.size());

try {

Connection con =   myds.getConnection();//从池中取一个连接

System.out.println("取出但未还,池中连接数:"+myds.pool.size());

System.out.println("当前连接对象:"+con);

Statement st =   con.createStatement();//问题是st为null,原因是MyConnection对于这个方法只是返回null

System.out.println(st);

con.close();//还回池中

} catch (SQLException e) {

e.printStackTrace();

}

System.out.println("用完后还了,池中连接数:"+myds.pool.size());

}

}

这种方法可以实现连接池,不过写的不够规范,下面介绍两种规范的方法

DBCP

1 什么是DBCP?

DBCP是Apache提供的一款开源免费的数据库连接池!

Hibernate3.0之后不再对DBCP提供支持!因为Hibernate声明DBCP有致命的缺欠!DBCP因为Hibernate的这一毁谤很是生气,并且说自己没有缺欠。

3

2 DBCP的使用

public void fun1() throws   SQLException {

BasicDataSource ds = new   BasicDataSource();

ds.setUsername("root");

ds.setPassword("123");

ds.setUrl("jdbc:mysql://localhost:3306/mydb1");

ds.setDriverClassName("com.mysql.jdbc.Driver");

ds.setMaxActive(20);

ds.setMaxIdle(10);

ds.setInitialSize(10);

ds.setMinIdle(2);

ds.setMaxWait(1000);

//最大等待毫秒数

Connection con = ds.getConnection();

System.out.println(con.getClass().getName());

con.close();

//关闭连接只是把连接归还给池!

}

通过BasicDataSourceFactory来生成BasicDataSource!BasicDataSourceFactory 可以通过Properties来创建BasicDataSource对象。

Properties prop = new Properties();

InputStream inStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("dbcp.properties");

prop.load(inStream);

//把配置信息都放到dbcp.properites配置文件中。然后再把配置文件加载到Properties对象中

BasicDataSource ds = (BasicDataSource)   BasicDataSourceFactory.createDataSource(prop);

//使用配置文件生成连接池对象。

Connection con = ds.getConnection();

System.out.println(con.getClass().getName());

con.close();

3 DBCP的配置信息

下面是对DBCP的配置介绍:

#基本配置

driverClassName=com.mysql.jdbc.Driver

url=jdbc:mysql://localhost:3306/mydb1

username=root

password=123

#初始化池大小,即一开始池中就会有10个连接对象

默认值为0

initialSize=0

#最大连接数,如果设置maxActive=50时,池中最多可以有50个连接,当然这50个连接中包含被使用的和没被使用的(空闲)

#你是一个包工头,你一共有50个工人,但这50个工人有的当前正在工作,有的正在空闲

#默认值为8,如果设置为非正数,表示没有限制!即无限大

maxActive=8

#最大空闲连接

#当设置maxIdle=30时,你是包工头,你允许最多有20个工人空闲,如果现在有30个空闲工人,那么要开除10个

#默认值为8,如果设置为负数,表示没有限制!即无限大

maxIdle=8

#最小空闲连接

#如果设置minIdel=5时,如果你的工人只有3个空闲,那么你需要再去招2个回来,保证有5个空闲工人

#默认值为0

minIdle=0

#最大等待时间

#当设置maxWait=5000时,现在你的工作都出去工作了,又来了一个工作,需要一个工人。

#这时就要等待有工人回来,如果等待5000毫秒还没回来,那就抛出异常

#没有工人的原因:最多工人数为50,已经有50个工人了,不能再招了,但50人都出去工作了。

#默认值为-1,表示无限期等待,不会抛出异常。

maxWait=-1

#连接属性

#就是原来放在url后面的参数,可以使用connectionProperties来指定

#如果已经在url后面指定了,那么就不用在这里指定了。

#useServerPrepStmts=true,MySQL开启预编译功能

#cachePrepStmts=true,MySQL开启缓存PreparedStatement功能,

#prepStmtCacheSize=50,缓存PreparedStatement的上限

#prepStmtCacheSqlLimit=300,当SQL模板长度大于300时,就不再缓存它

connectionProperties=useUnicode=true;characterEncoding=UTF8;useServerPrepStmts=true;cachePrepStmts=true;prepStmtCacheSize=50;prepStmtCacheSqlLimit=300

#连接的默认提交方式

#默认值为true

defaultAutoCommit=true

#连接是否为只读连接

#Connection有一对方法:setReadOnly(boolean)和isReadOnly()

#如果是只读连接,那么你只能用这个连接来做查询

#指定连接为只读是为了优化!这个优化与并发事务相关!

#如果两个并发事务,对同一行记录做增、删、改操作,是不是一定要隔离它们啊?

#如果两个并发事务,对同一行记录只做查询操作,那么是不是就不用隔离它们了?

#如果没有指定这个属性值,那么是否为只读连接,这就由驱动自己来决定了。即Connection的实现类自己来决定!

defaultReadOnly=false

#指定事务的事务隔离级别

#可选值:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE

#如果没有指定,那么由驱动中的Connection实现类自己来决定

defaultTransactionIsolation=REPEATABLE_READ

C3P0

1 C3P0简介

  C3P0也是开源免费的连接池!C3P0被很多人看好!

2 C3P0的使用

  C3P0中池类是:ComboPooledDataSource。

public void fun1() throws   PropertyVetoException, SQLException {

ComboPooledDataSource ds = new   ComboPooledDataSource();

//基本配置

ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb1");

ds.setUser("root");

ds.setPassword("123");

ds.setDriverClass("com.mysql.jdbc.Driver");

//每次的增量为5

ds.setAcquireIncrement(5);

//初始化连接数

ds.setInitialPoolSize(20);

//最少连接数

ds.setMinPoolSize(2);

//最多连接数

ds.setMaxPoolSize(50);

Connection con = ds.getConnection();

System.out.println(con);

con.close();

}

c3p0也可以指定配置文件,而且配置文件可以是properties,也可是xml的。当然xml的高级一些了。但是c3p0的配置文件名必须为c3p0-config.xml,并且必须放在类路径下。

<?xml version="1.0"   encoding="UTF-8"?>

<c3p0-config>

<default-config>

<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property>

<property name="driverClass">com.mysql.jdbc.Driver</property>

<property name="user">root</property>

<property name="password">123</property>

<property name="acquireIncrement">3</property>

<property name="initialPoolSize">10</property>

<property name="minPoolSize">2</property>

<property name="maxPoolSize">10</property>

</default-config>

<named-config name="oracle-config">

<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property>

<property name="driverClass">com.mysql.jdbc.Driver</property>

<property name="user">root</property>

<property name="password">123</property>

<property name="acquireIncrement">3</property>

<property name="initialPoolSize">10</property>

<property name="minPoolSize">2</property>

<property name="maxPoolSize">10</property>

</named-config>

</c3p0-config>

  c3p0的配置文件中可以配置多个连接信息,可以给每个配置起个名字,这样可以方便的通过配置名称来切换配置信息。上面文件中默认配置为mysql的配置,名为oracle-config的配置也是mysql的配置,呵呵。

public void fun2() throws   PropertyVetoException, SQLException {

ComboPooledDataSource ds = new   ComboPooledDataSource();

/*

不用定配置文件名称,因为配置文件名必须是c3p0-config.xml,这里使用的是默认配置。

*/

Connection con = ds.getConnection();

System.out.println(con);

con.close();

}

public void fun2() throws PropertyVetoException,   SQLException {

ComboPooledDataSource ds = new   ComboPooledDataSource("orcale-config");

//使用名为oracle-config的配置。

Connection con = ds.getConnection();

System.out.println(con);

con.close();

}