事务
事务概述
事务的四大特性(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(); } |