20.JDBC开发(3)事务和池(我的JavaEE笔记)

主要内容:

  • 事务
  • 使用数据库连接池优化程序性能

一、事务的概念

事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功。数据库开启事务命令:

  • start transaction 开启事务
  • rollback 回滚事务
  • commit 提交事务

当我们开启事务后,可输入多条sql语句让数据库执行,但是如果我们在让sql语句执行之后最后没有使用commit提交事务,则前面执行的所有sql语句无效,这就相当于回到了开启事务之前的状态,当然有时候这种方式并不太好,我们可以自己设置回滚点,当我们sql语句出错时可以回到设置的那个点处的状态。而rollback可以每次回滚一条语句。

二、使用事务

  • 当jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它前面发送的sql语句。若向关闭这种默认提交方式,让多条sql在一个事务中执行,可使用下列语句:
    jdbc控制事务语句
    connection.setAutoCommit(false); start transaction
    connection.rollback(); rollback
    connection.commit(); commit
    设置事务回滚点
    Savepoint sp = conn.setSavepoint();
    conn.rollback(sp);
    conn.commit(); //回滚后必须要提交

例:
创建数据库:

create database day16;
CREATE TABLE account(
        id INT PRIMARY KEY AUTO_INCREMENT,
        NAME VARCHAR(40),
        money FLOAT
)CHARACTER SET utf8 COLLATE utf8_general_ci;
INSERT INTO account(NAME,money) VALUES('aaa',1000);
INSERT INTO account(NAME,money) VALUES('bbb',1000);
INSERT INTO account(NAME,money) VALUES('ccc',1000);

Demo1.java

package cn.itcast.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import cn.itcast.utils.JdbcUtils;

public class Demo1 {

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement  ps = null;
        ResultSet result = null;
        try {
            conn = JdbcUtils.getConnection();
            String sql1 = "update account set money=money-100 where name='aaa'";
            String sql2 = "update account set money=money+100 where name='bbb'";
            conn.setAutoCommit(false);//开启事务
            ps = conn.prepareStatement(sql1);
            ps.executeUpdate();
            //int x = 1/0;//模拟异常,sql语句不会执行
            ps = conn.prepareStatement(sql2);
            ps.executeUpdate();
            System.out.println("ok");
        } catch (Exception e) {
            try {
                conn.rollback();//手动通知数据库手动回滚
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }finally{
            JdbcUtils.release(conn, ps, result);
        }
    }
}

例:设置回滚点
Demo2.java

package cn.itcast.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import cn.itcast.utils.JdbcUtils;

public class Demo2 {

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet result = null;
        Savepoint point = null;
        try{
            conn = JdbcUtils.getConnection();
            String sql1 = "update account set money = money - 100 where name = 'aaa'";
            String sql2 = "update account set money = money + 100 where name = 'bbb'";
            String sql3 = "update account set money = money + 100 where name = 'ccc'";
            conn.setAutoCommit(false);
            ps = conn.prepareStatement(sql1);
            ps.executeUpdate();
            
            point = conn.setSavepoint();//设置回滚点
            
            ps = conn.prepareStatement(sql2);
            ps.executeUpdate();
            
            //int x = 1/0;
            
            ps = conn.prepareStatement(sql3);
            ps.executeUpdate();
            
            conn.commit();
        }catch(Exception e){
            try {
                conn.rollback(point);//手动通知回滚,同时指定回滚点
                //回滚之后记得提交,上面我们回滚了,就表明最后的提交语句没有执行,那此时如果不提交
                //,数据库在没有收到提交的情况下,会自动回滚所有的sql语句
                conn.commit();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }finally{
            JdbcUtils.release(conn, ps, result);
        }   
    }
}

说明:这里如果中间出现异常,则只有第一条语句生效。

三、事务的特性(ACID)

  • 原子性(Atomicity)
    原子性是指事务是一个不可分割的工作单位,事务中的操作要么都是执行成功,要么都失败。

  • 一致性(Consistency)
    事务必须使数据库从一个一致性状态变换到另外一个一致性状态。比如,在转账中账户的总额是不变的。

  • 隔离性(Isolation)
    事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

  • 持久性(Durability)
    持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

四、 事务的隔离级别

多个线程开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。

如果不考虑隔离性,可能会引发如下问题:

  • 脏读
    指一个事务读取了另外一个事务未提交的数据。
    例如:a花钱让b给办点事,a向b转账之后但未提交,但是b此时会发现账户多了钱,然后将事办完之后a却不提交,此时b的账户的钱就会变回原来的数目,相当于白干活了。

  • 不可重复读
    在一个事务内读取表中的某一行数据,多次读取的结果不同。
    如a开启一个事务后,查询余额为200,此时b转账100,那么a此时查询就是300,两次结果不一致。当然有些时候这样是正确的,但是有时候却不是,如在统计时我们不能让多次的统计结果不一致。
    和脏读的区别:脏读是读取前一事务未提交的数据,不可重复读是重新读取了前一事务已经提交的数据。

  • 虚读(幻读)
    是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
    比如一个表第一次查询有2条数据,此时另外一个事务插入了一条数据,此时再次查询就变成了3条数据,两次查询结果不一致。
    和不可重复读的区别:不可重复读是读取到的数据结果不同,而虚读是指读取到多个事务导致结果不一致。

五、事务隔离性的设置语句

  • 数据库共定义了四种隔离级别:
    Serializable:可避免脏读、不可重复读、虚读情况的发生。(串行化)
    Repeatable read:可避免脏读、不可重复读情况的发生。(可重复读)
    Read committed:可避免脏读情况发生(读已提交)。
    Read uncommitted:最低级别,以上情况均无法保证。(读未提交)

  • set transaction isolation level设置事务隔离级别(数据库操作)

  • select @@tx_isolation查询当前事务隔离级别(数据库操作)

例:设置隔离级别

Demo3.java

package cn.itcast.demo;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import cn.itcast.utils.JdbcUtils;

public class Demo3 {

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet result = null;
        try {
            conn = JdbcUtils.getConnection();
            //查询程序肯定至少要到这个级别
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            conn.setAutoCommit(false);
            String sql = "select * from account where name='aaa'";
            ps = conn.prepareStatement(sql);
            result = ps.executeQuery();
            if(result.next()){
                System.out.println(result.getFloat("money"));
            }
            Thread.sleep(1000*10);
            result = ps.executeQuery();
            if(result.next()){
                System.out.println(result.getFloat("money"));
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            JdbcUtils.release(conn, ps, result);
        }
    }

}

六、使用数据库连接池优化程序性能

应用程序直接获取连接的缺点:用户每次都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。


20.JDBC开发(3)事务和池(我的JavaEE笔记)_第1张图片
1.png

这时我们可以使用数据库连接池优化程序性能:


20.JDBC开发(3)事务和池(我的JavaEE笔记)_第2张图片
2.png
  • 编写连接池需要实现java.sql.DataSource接口。DataSource接口中定义了两个重载的getConnection方法:
    Connection getConnection()
    Connection getConnection(String username,String password)

  • 实现DataSource接口,并实现连接池功能的步骤:
    1.在DataSource构造函数中批量创建与数据库的连接,并把创建的连接加入LinkedList对象中。
    2.实现getConnection方法,让getConnection方法每次调用时,从LinkedList中取一个Connection返回给用户。
    3.当用户使用完Connection,调用Connection.close()方法时,Collection对象应保证将自己返回到LinkedList中,而不要把Collection还给数据库。
    Collection保证将自己返回到LinkedList中是此处编程的难点。

示例:模拟数据库连接池
JdbcPool.java

package junit.test;
import java.io.InputStream;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.LinkedList;
import java.util.Properties;
import java.util.logging.Logger;
import javax.sql.DataSource;

public class JdbcPool implements DataSource {
    
    //后面涉及到大量的增删改查,所以使用LinkedList类
    private static LinkedList list = new LinkedList();
    
    static {
        try {
            InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("db.properties");
            Properties properties = new Properties();
            properties.load(in);
            String driver = properties.getProperty("driver");
            String url = properties.getProperty("url");
            String username = properties.getProperty("username");
            String password = properties.getProperty("password");
            
            Class.forName(driver);
            
            for(int i = 0; i < 10; i++){
                Connection conn = DriverManager.getConnection(url, username, password);
                list.add(conn);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        if(list.size() > 0){
            //如果连接池中还有Connection连接则从池中删除并返回给调用者
            Connection conn = list.removeFirst();
            return conn;
        }else{
            throw new RuntimeException("数据库正忙");
        }
    }

    其他需要实现的方法我们并不关心,这里省略
}

说明:

  • 这里有个问题是如果调用者使用完Connection链接之后调用方法conn.close();那么此链接将不会返回给数据库连接池,而是直接返回给了数据库,这样链接会用一个少一个,显然不行。也就是说不能这样,或者说close方法不够用,我们需要增强一下,让其不要返还给数据库,而是返还给连接池。

  • 对于类的某个方法不能达到我们的要求时需要对其进行增强,而增强的方式有三种:1.写一个子类,覆盖其close方法;2.写一个Connection的包装类,增强close方法;3.使用动态代理,返回一个代理对象出去,拦截close方法的调用,达到对close方法增强的功能。

  • 第一种不行,因为我们在要返回Connection的时候对象中已经封装了相关信息,即使我们写一个子类也仅仅表明此子类有和父类相同的功能,但是却没有父类中已经封装好了的信息,不能对数据库进行操作。

  • 第二种:写一个包装类。

/*
 * 用包装设计模式对某个对象进行增强步骤:
 * 1、写一个类,实现与被增强对象(这里要增强的对象是mysql的连接对象connection)相同的接口(这里的接口是Connection)
 * 2.定义一个变量,指向被增强对象
 * 3、定义一个构造方法,接收被增强对象(也就是将我们要增强的对象传递进来进行增强)
 * 4、覆盖想增强的方法(这里是close方法)、
 * 5、对于不想增强的方法,直接调用被增强对象的方法,如this.conn.unwrap(iface)
 * */
class MyConnection implements Connection{
    
    private Connection conn ;
    private List pool;//这里我们需要将数据库连接池传递进来,因为之后我们使用的链接都是增强之后的链接
    
    public MyConnection() {
    }
    public MyConnection(Connection conn , List pool){
        this.conn = conn;
        this.pool = pool;
    }
    
    //这里我们只是需要增强close方法,其他方法直接调用父类的方法即可
    @Override
    public void close() throws SQLException {
        pool.add(conn);
    }
    
    @Override
    public  T unwrap(Class iface) throws SQLException {
        return this.conn.unwrap(iface);
    }
其他方法和上面这个方法类似,此处省略
}

说明:之后返回就不是返回Connection对象了,而是return new MyConnection(conn, list);,但是显然我们可以看到这种方式太麻烦,因为其中的方法太多。

  • 第三种:使用动态代理
@Override
    public Connection getConnection() throws SQLException {
        if(list.size() > 0){
            //如果连接池中还有Connection连接则从池中删除并返回给调用者
            final Connection conn = list.removeFirst();
            //第一个参数指的是使用哪个类装载器,第二个参数指明我们要对那个对象进行增前,第三个参数指明增强对象完成什么功能
            return (Connection) Proxy.newProxyInstance(JdbcPool.class.getClassLoader(), conn.getClass().getInterfaces(), 
                    new InvocationHandler() {
                //使用动态代理之后其实不管之后我们调用Connection的什么方法(commit、rollback...)其实都是调用下面的invoke方法
                @Override
                public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
                    //如果调用的方法不是close方法,那么我们使用原来Connection的方法
                    if(!method.getName().equals("close")){
                        return method.invoke(conn, args);
                    }else{
                        //如果调用close方法,我们将链接返还给数据库连接池
                        return list.add(conn);
                    }
                }
            });
        }else{
            throw new RuntimeException("数据库正忙");
        }
    }

说明:其实动态代理是使用的拦截技术,这里我们不详细讲,在后面将过滤器会详细说明。

那么我们可以对之前的数据库工具类做一些改进:
JdbcUtils.java

package junit.test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcUtils {

    private static JdbcPool pool = new JdbcPool();
    public static Connection getConnection() throws SQLException{
        return pool.getConnection();
    }
    
    public static void release(Connection conn, Statement ps , ResultSet result){
        if(result != null){
            try {
                result.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            result = null;
        }
        if(ps != null){
            try {
                ps.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            ps = null;
        }
        if(conn != null){
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }
}

你可能感兴趣的:(20.JDBC开发(3)事务和池(我的JavaEE笔记))