Bootstrap

学习Java的日子 Day55 事务,批处理

Day 55

1.数据库知识点的梳理

1.数据库概念(数据库服务器、数据库、数据表、字段、数据行)

2.使用SQL(DDL、DML、DCL)

3.深入SQL(数据类型、约束、索引、视图、触发器、存储过程、函数)

4.JDBC(使用Java语法操作数据库的技术,Connection、Statement、ResultSet)

5.SQL注入

6.封装DBUtil - v1.0(使用配置文件 - DBConfig.properties)

7.封装DBUtil - v2.0(更新方法、主键回填方法、查询方法)

8.事务

9.封装DBUtil - v3.0(封装事务)

10.批处理

11.CBLob - 了解

12.自定义连接池

13.Druid连接池

14.封装DBUtil - v3.0(封装连接池)

2.事务(重要)

理解:MySQL默认一条SQL命令就是一个事务,事务默认自动管理提交

一个业务需要发送多条SQL语句给数据库执行。需要将多次访问数据库的操作视为一个整体来执行,要么所有的SQL语句全部执行成功。如果其中有一条SQL语句失败,就进行事务的回滚,所有的SQL语句全部执行失败。

开启事务 : START TRANSACTION;

提交事务 : COMMIT;

回滚事务 : ROLLBACK;

经验:

如果想让多条SQL命令一起完成,就必须将多条SQL命令添加到同一个事务里,因为事务有原子性(要么同时完成,要么同时不完成)

事务执行中如果出现的bug,可以回滚事务(将数据恢复至开启事务时的状态)

需求: 模拟银行用户转账

START TRANSACTION;# 开启事务

UPDATE bank SET money=money-200 WHERE id=1;
UPDATE bank SET money=money+200 WHERE id=2;

COMMIT;# 提交事务

ROLLBACK;# 回滚事务

设置表

在这里插入图片描述

提交事务

在这里插入图片描述

回滚事务 (不变)

在这里插入图片描述

2.1 使用JDBC操作事务

JDBC控制事务语句:

Connection.setAutoCommit(false);

Connection.rollback();

Connection.commit();

模拟一个转账的功能,用事务来控制

需求:模拟银行用户转账

A 2000

B 2000

A — B 200

public static void main(String[] args) {

        Connection connection = null;
        PreparedStatement statement1 = null;
        PreparedStatement statement2 = null;
        try {
            connection = DBUtil.getConnection();

            //开启事务
            connection.setAutoCommit(false);

            String sql1 = "UPDATE bank SET money=money-200 WHERE id=1;";
            statement1 = connection.prepareStatement(sql1);
            statement1.executeUpdate();

            //设置一个错误,出错了就回滚事务,回滚到开启事务的时候
            //System.out.println(10/0);

            String sql2 = "UPDATE bank SET money=money+200 WHERE id=2;";
            statement2 = connection.prepareStatement(sql2);
            statement2.executeUpdate();

            //提交事务
            //程序运行到此处,说明没有出现任何问题,则需要提交事务
            connection.commit();

        } catch (Exception e) {

            //回滚事务
            //程序在出现异常时会执行到这个地方,此时就需要回滚事务
            if(connection != null){
                try {
                    connection.rollback();
                } catch (SQLException ex) {
                    throw new RuntimeException(ex);
                }
            }

        } finally {
            DBUtil.close(null,statement2,null);
            DBUtil.close(connection,statement1,null);
        }

    }
}

connection.setAutoCommit(false);

默认为true,自动提交;改成false后,变为手动提交, 此时我们执行的DML语句都不会提交, 需要手动的执行commit进行提交。

2.2 封装事务 v1.0

需求:模拟银行用户转账

程序设计的要求:

设计存钱和取钱的方法

public static void saveMoney(int id,float money) – 存钱

public static void withdrawMoney(int id,float money) – 取钱

单独调用存钱和取钱的方法

联合调用存钱和取钱的方法 – 转账

注意:

场景一:单独调用存钱和取钱的方法,不同的业务,使用不同的connection对象

场景二:转账需要开启事务,要使用同一个connection对象,所以在工具类把connection设置成了静态的

public class Test01 {
    public static void main(String[] args) {

        //场景一:单独调用存钱和取钱的方法
//        try {
//            saveMoney(1,200);
//        } catch (SQLException e) {
//            throw new RuntimeException(e);
//        }
//
//        try {
//            withdrawMoney(2,300);
//        } catch (SQLException e) {
//            throw new RuntimeException(e);
//        }


        //场景二:转账
        try {

            DBUtil.startTransaction();

            withdrawMoney(1,200);

            System.out.println(10/0);

            saveMoney(2,200);

            DBUtil.commit();

        } catch (Exception e) {
            try {
                DBUtil.rollback();
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            }
        }

    }

    public static void withdrawMoney(int id,float money) throws SQLException {
        Connection connection = null;
        PreparedStatement statement = null;

        try {
            connection = DBUtil.getConnection();

            System.out.println(connection);

            String sql = "update bank set money=money-? where id=?";
            statement = connection.prepareStatement(sql);
            statement.setFloat(1,money);
            statement.setInt(2,id);
            statement.executeUpdate();

        } finally {
            DBUtil.close(connection,statement,null);
        }
    }

    public static void saveMoney(int id,float money) throws SQLException {
        Connection connection = null;
        PreparedStatement statement = null;

        try {
            connection = DBUtil.getConnection();

            System.out.println(connection);

            String sql = "update bank set money=money+? where id=?";
            statement = connection.prepareStatement(sql);
            statement.setFloat(1,money);
            statement.setInt(2,id);
            statement.executeUpdate();

        } finally {
            DBUtil.close(connection,statement,null);
        }
    }

}

数据库工具类

public class DBUtil {

    private static String url;
    private static String username;
    private static String password;

    static{
        Properties properties = new Properties();
        try {
            properties.load(DBUtil.class.getClassLoader().getResourceAsStream("DBConfig.properties"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        String driverName = properties.getProperty("driverName");
        url = properties.getProperty("url");
        username = properties.getProperty("username");
        password = properties.getProperty("password");

        try {
            Class.forName(driverName);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    private static Connection connection = null; //重要

    /**
     * 获取连接对象
     */
    public static Connection getConnection() throws SQLException {
        if(connection == null){
            connection = DriverManager.getConnection(url,username,password);
        }
        return connection;
    }

    /**
     * 关闭资源
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet){
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(connection != null){
            try {
                if(connection.getAutoCommit()){
                    connection.close();
                    DBUtil.connection = null;
                }
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 开启事务
     */
    public static void startTransaction() throws SQLException {
        Connection connection = getConnection();
        connection.setAutoCommit(false);
    }

    /**
     * 提交事务
     */
    public static void commit() throws SQLException {
        if(connection != null){
            connection.commit();
            connection.close();
            connection = null;
        }
    }

    /**
     * 回滚事务
     */
    public static void rollback() throws SQLException {
        if(connection != null){
            connection.rollback();
            connection.close();
            connection = null;
        }
    }

    /**
     * 更新数据(添加、删除、修改)
     */
    public static int commonUpdate(String sql,Object... params) throws SQLException {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = getConnection();
            statement = connection.prepareStatement(sql);
            paramHandler(statement,params);
            int num = statement.executeUpdate();
            return num;
        }finally {
            close(connection,statement,null);
        }
    }

   //后面的方法省略

connection.close(); 关闭不等于空,还存在connection对象

2.3 封装事务 v2.0 ThreadLocal

要在多线程下使用,转账就是一个线程,很多个人去转账,即多个线程去操作同一个资源(connection对象),并且多个线程的功能是一样的,和卖票的案例一样。加锁还不能解决问题,加了锁,一个线程转账,其他线程都要阻塞等待

上面的使用private static Connection connection = null; 一个线程操作了该connection对象,其他线程就只有在外面阻塞,不符合银行转账的功能,不同线程操作同一个共享变量,自己线程的变量可以改变,不影响其他线程的变量,即ThreadLocal

数据库工具类

package com.qf.utils;

import java.io.IOException;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

public class DBUtil {

    private static String url;
    private static String username;
    private static String password;

    private static ThreadLocal<Connection> local;

    static{
        Properties properties = new Properties();
        try {
            properties.load(DBUtil.class.getClassLoader().getResourceAsStream("DBConfig.properties"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        String driverName = properties.getProperty("driverName");
        url = properties.getProperty("url");
        username = properties.getProperty("username");
        password = properties.getProperty("password");

        try {
            Class.forName(driverName);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        local = new ThreadLocal<>();
    }

    /**
     * 获取连接对象
     */
    public static Connection getConnection() throws SQLException {
        Connection connection = local.get();//获取当前线程的Connection对象
        if(connection == null){
            connection = DriverManager.getConnection(url,username,password);
            local.set(connection);//将Connection对象添加到local中
        }
        return connection;
    }

    /**
     * 关闭资源
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet){
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(connection != null){
            try {
                if(connection.getAutoCommit()){
                    connection.close();
                    local.set(null);
                }
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 开启事务
     */
    public static void startTransaction() throws SQLException {
        Connection connection = getConnection();
        connection.setAutoCommit(false);
    }

    /**
     * 提交事务
     */
    public static void commit() throws SQLException {
        Connection connection = local.get();
        if(connection != null){
            connection.commit();
            connection.close();
            local.set(null);
        }
    }

    public static void rollback() throws SQLException {
        Connection connection = local.get();
        if(connection != null){
            connection.rollback();
            connection.close();
            local.set(null);
        }
    }

    /**
     * 更新数据(添加、删除、修改)
     */
    public static int commonUpdate(String sql,Object... params) throws SQLException {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = getConnection();
            statement = connection.prepareStatement(sql);
            paramHandler(statement,params);
            int num = statement.executeUpdate();
            return num;
        }finally {
            close(connection,statement,null);
        }
    }

    //后面的方法省略

3.事务的特点

事务的特性:ACID

原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )

原子性:事务是数据库的逻辑工作单位,事务中包含的各操作要么都完成,要么都不完成

事务中的多条SQL语句要么全部成功,要么全部失败。

一致性:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。

多条SQL语句全部提交成功时,就可以叫做从一个一致性状态变到另一个一致性状态

如果在执行出现错误,有一部分执行了,有一部分没有执行,那么就是不一致状态

隔离性:一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

在并发操作的情况下,多个事务互不干扰,一个事务的成功或者失败对于其他的事务是没有影响。

持久性:指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

事务一旦提交成功,它对数据库中的数据的改变就是永久的。

3.1 事务的隔离级别

事务的隔离级别属于事务的。都已开启了事务为前提。

不考虑事务的隔离级别,会出现以下的情况:

脏读:一个线程中的事务读到了另外一个线程中未提交的数据。

不可重复读:一个线程中的事务读到了另外一个线程中已经提交的update的数据。

虚读:一个线程中的事务读到了另外一个线程中已经提交的insert的数据。

要想避免以上现象,通过更改事务的隔离级别来避免:

1.READ UNCOMMITTED (读未提交) 脏读、不可重复读、虚读有可能发生。

2.READ COMMITTED (读提交) 避免脏读的发生,不可重复读、虚读有可能发生。

3.REPEATABLE READ (可重复读取) 避免脏读、不可重复读的发生,虚读有可能发生。

4.SERIALIZABLE (可串行化) 避免脏读、不可重复读、虚读的发生。

隔离级别脏读不可重复读虚读
READ UNCOMMITTED (读未提交)
READ COMMITTED (读提交)×
REPEATABLE READ (可重复读取)××
SERIALIZABLE (可串行化)×××

级别依次升高,效率依次降低;选择什么隔离级别,看项目需求

MySQL:默认REPEATABLE READ**(可重复读取)**

ORACLE:默认READ COMMITTED

MySQL:

SELECT @@SESSION.transaction_isolation;//查看当前的隔离级别

set transaction isolation level 级别;// 设置当前的事务隔离级别

eg:

set transaction isolation level REPEATABLE READ;

3.2 JDBC设置隔离级别

Connection对象,在开启事务之前进行设置

在这里插入图片描述

4.批处理

理解:批量处理数据

原理:将多条SQL命令添加到Batch包中,JDBC将Batch包发送给数据库管理系统,数据库管理系统再做批量操作

注意:URL添加上rewriteBatchedStatements=true,否则会数据库驱动无视Batch包

场景一:多条SQL命令不一样的情况

public class Test01 {
    public static void main(String[] args) throws SQLException {

        Connection connection = DBUtil.getConnection();
        Statement statement = connection.createStatement();

        //将多条sql命令添加到Batch包中
        String sql1 = "insert into student(name,course_id,class_id,age) values('ccc',1,3,20)";
        String sql2 = "update student set age=19 where id=2";
        statement.addBatch(sql1);
        statement.addBatch(sql2);

        //发送Batch包
        statement.executeBatch();

        DBUtil.close(connection,statement,null);
    }
}

场景二:多条SQL命令一样但参数不一样的情况

public class Test02 {
    public static void main(String[] args) throws SQLException {

        Connection connection = DBUtil.getConnection();

        String sql = "insert into student(name,course_id,class_id,age) values(?,1,3,20)";
        PreparedStatement statement = connection.prepareStatement(sql);

        for (int i = 1; i <= 20; i++) {
            statement.setString(1,"小黑"+i);
            statement.addBatch();
        }

        statement.executeBatch();

        DBUtil.close(connection,statement,null);

    }
}

场景三:多条SQL命令一样但参数不一样的情况 – 大量数据

在我们进行大批量数据操作的时候,需要采用批处理的方式来提高程序的运行性能,目的是减少跟数据库交互的次数

经验:

1.分批次发送Batch包 – 减少数据库压力

2.使用事务 ---------- 让数据库预处理

使用事务,等所有的数据加载到jdbc里,才能提交事务,只与jdbc交互一次

public class Test03 {
    public static void main(String[] args) throws SQLException {

        Connection connection = DBUtil.getConnection();

        connection.setAutoCommit(false);

        String sql = "insert into student(name,course_id,class_id,age) values(?,1,3,20)";
        PreparedStatement statement = connection.prepareStatement(sql);

        for (int i = 1; i <= 20000; i++) {
            statement.setString(1,"小红"+i);
            statement.addBatch();

            if(i%1000 == 0){
                statement.executeBatch();
                statement.clearBatch();//清空Batch包中的数据
            }
        }

        connection.commit();

        DBUtil.close(connection,statement,null);

    }
}

5.CBLob(了解即可)

理解:

JDBC可以向数据库存储二进制数据或长文本数据

BLob - Binary(二进制)Lob - 存储二进制数据 - 注意:数据库字段类型 - BLOB/LONGBLOB

CLob - Character(字符)Lob - 存储长文本数据 - 注意:数据库字段类型 - TEXT/LONGTEXT

经验:

了解CBLob的技术

在项目中不会把文件写入到数据库中(因为写入到数据库中,数据库的压力会很大)

在项目中如果要存储文件,可以将文件的地址写入到数据库中

BLob:

public class Test01 {

    @Test
    public void test01() throws SQLException, FileNotFoundException {
        //将图片插入到数据库中

        Connection connection = DBUtil.getConnection();

        String sql = "insert into cblob(b_lob) values (?)";
        PreparedStatement statement = connection.prepareStatement(sql);

        InputStream in = new FileInputStream("葵司.jpg");
        statement.setBinaryStream(1,in);
        statement.executeUpdate();

        DBUtil.close(connection,statement,null);
    }

    @Test
    public void test02() throws SQLException, IOException {
        //将数据库中的图片读取到本地

        Connection connection = DBUtil.getConnection();

        String sql = "select * from cblob where id=1";
        PreparedStatement statement = connection.prepareStatement(sql);

        ResultSet resultSet = statement.executeQuery();
        if(resultSet.next()){

            InputStream in = resultSet.getBinaryStream("b_lob");
            FileOutputStream out = new FileOutputStream("copyImg.jpg");
            byte[] bs = new byte[1024];
            int len;
            while((len = in.read(bs)) != -1){
                out.write(bs,0,len);
            }

            in.close();
            out.close();
            DBUtil.close(connection,statement,resultSet);
        }


    }

}

CLob :

public class Test02 {

    @Test
    public void test01() throws SQLException, FileNotFoundException {
        //将长文本数据插入到数据库中

        Connection connection = DBUtil.getConnection();

        String sql = "insert into cblob(c_lob) values (?)";
        PreparedStatement statement = connection.prepareStatement(sql);

        FileReader reader = new FileReader("小说.txt");
        statement.setCharacterStream(1,reader);
        statement.executeUpdate();

        DBUtil.close(connection,statement,null);
    }

    @Test
    public void test02() throws SQLException, IOException {
        //将数据库中的长文本数据读取到本地

        Connection connection = DBUtil.getConnection();

        String sql = "select * from cblob where id=2";
        PreparedStatement statement = connection.prepareStatement(sql);

        ResultSet resultSet = statement.executeQuery();
        if(resultSet.next()){

            Reader reader = resultSet.getCharacterStream("c_lob");
            FileWriter writer = new FileWriter("copyText.txt");
            char[] cs = new char[1024];
            int len;
            while((len = reader.read(cs)) != -1){
                writer.write(cs,0,len);
            }

            reader.close();
            writer.close();
            DBUtil.close(connection,statement,resultSet);
        }

    }

}

总结

1.事务
1.SQL命令
2.在JDBC中操作事务
3.事务封装DBUtil – 重要
4.事务的特性(ACID) – 一定要会用自己的语言描述清楚(跟面试有关)
5.事务的隔离界别 – 做实验

2.批处理

;