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);
        }
    }

}

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

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


1.png

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


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<Connection> list = new LinkedList<Connection>();
    
    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> T unwrap(Class<T> 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;
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • JDBC概述 在Java中,数据库存取技术可分为如下几类:JDBC直接访问数据库、JDO技术、第三方O/R工具,如...
    usopp阅读 3,522评论 3 75
  • JDBC简介 SUN公司为了简化、统一对数据库的操作,定义了一套Java操作数据库的规范,称之为JDBC。JDBC...
    奋斗的老王阅读 1,490评论 0 51
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,504评论 18 399
  • 本文包括传统JDBC的缺点连接池原理自定义连接池开源数据库连接池DBCP连接池C3P0连接池Tomcat内置连接池...
    廖少少阅读 16,689评论 0 37
  • 提起了的笔总是会放下,我怕把你写进我的故事里。 多年后也许我还会想起,曾经有那么一个人,无关风花雪月,只是我...
    好人坏人阅读 380评论 0 2