Java JDBC

Java事务

通俗的理解,事务是一组原子操作单元,从数据库角度说,就是一组SQL指令,要么全部执行成功,若因为某个原因其中
T条指令执行有错误,则撤销先前执行过的所有指令。更简答的说就是:要么全部执行成功,要么撤销不执行。

事务的ACID特性

  • 原子性:表示事务执行过程中的任何失败都将导致事务所做的任何修改失效。
  • 一致性:表示当事务执行失败时,所有被该事务影响的数据都应该恢复到事务执行前的状态
  • 隔离性:表示事务执行过程中对数据的修改,在事务提交之前对其他事务不可见
  • 持久性:表示已提交的数据在事务执行失败时,数据的状态都应该是正确的

事务主要是在对数据库的增删改操作中存在,查询影响的是隔离性。

案例

​ 现在我们需要模拟一个场景:A银行账户给B银行账户转账,那么我们需要在A用户上扣钱,在B用户上加钱。所以我们需要去写两条SQL语句来完成这样的需求,但是我们要开启事务的支持,因为这两条SQL是在一条事务线上的,所以,要么同时执行成功,要么同时执行失败,满足事务的原子性和一致性。

​ 首先,我们如果没开启对事务的支持,那么在A扣钱成功之后、B加钱成功之前如果发生一个异常,这里模拟为一个算术异常,这个异常会被catch语句块捕获,捕获后B用户加钱的SQL就不会被执行,那么这样A扣了钱,但是B却没有接收到钱,这便是破坏了事务的一致性与原子性。

Connection connection=null;
Statement statement=null;
try {
    Class.forName("com.mysql.cj.jdbc.Driver");
    connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=GMT", "root", "123456");
    connection.setAutoCommit(false);

    statement=connection.createStatement();

    //A账户扣钱
    int num = statement.executeUpdate("update account set balance_A=balance_A-1000 where id=1");
    System.out.println(num==1?"A扣钱成功":"A扣钱失败");

    //模拟一个异常 这个异常会被捕获 这样A的钱扣了 B的钱却没加上
    int i=1/0;


    //B账户加钱
    num=statement.executeUpdate("update account set balance_B=balance_B+1000 where id=1");
    System.out.println(num==1?"转账成功":"转账失败");

    //提交事务 在提交的时候才会去修改数据库
    connection.commit();


} catch (Exception e) {

    //事务回滚
    try {
        connection.rollback();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }

    e.printStackTrace();


}finally {
    if (statement!=null) {
        try {
            statement.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
    if (connection!=null) {
        try {
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

​ 在JDBC中是默认开启自动事务提交模式的,所谓自动提交模式就是,每条SQL都被当做一个事务,每执行完一条SQL,就是执行完一个事务那么自动提交这个事务对数据库做修改,我们这个业务的需求希望执行完A加钱SQL、B扣钱SQL语句后才算事务的结束,所以我们需要关闭自动事务提交模式,调用方法

connection.setAutoCommit(false);

设置参数false就是关系自动提交模式。然后在事务执行完成后,调用方法

connection.commit();

手动提交事务,表示这样该事务成功执行。

最后还有一个事务回滚的方法

connection.rollback();

如果出现了异常,那么就会回滚该事务,让数据库返回到事务执行之前的正常状态,通俗点也就是try里面写的对账户的SQL操作当它没执行过。

乐观锁与悲观锁

假设在多线程场景下,多个线程同时对一个数据库中的表进行操作,如果多个线程会去修改同一条数据。

乐观锁:

乐观锁不用去开启事务的支持,采取自动提交的策略。乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。

乐观锁的实现:

  1. CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。

  2. 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

    #影响记录条数只要大于或者等于1,版本号会加1
    update user set username ='tom' where id = 1 and version = "当前数据库中的 version 值"
    

说明
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

悲观锁:

悲观锁要开启事务的支持,采用手动提交的策略。当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)

悲观锁主要分为共享锁和排他锁:

  • 共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。

for update

for update 是一种行级锁,又叫排它锁。对于上面转账的案例,我们将转账拆分成两个程序,A转账给B,更新account表,修改A的余额;B接收钱,更新account表,修改B的余额。我们需要修改account表,此时我们要对该表加上排他锁,避免并发操作带来数据的不一致性。

A程序:

public static void main(String[] args) {

        Connection connection=null;
        Statement statement=null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=GMT", "root", "123456");
            connection.setAutoCommit(false);
            statement=connection.createStatement();

            //A账户扣钱
            //for update 加上写锁
            statement.executeQuery("select * from account  where id=1 for update ");

            //提交事务 在提交的时候才会去修改数据库
            connection.commit();

        } catch (Exception e) {

            //事务回滚
            try {
                connection.rollback();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }

            e.printStackTrace();


        }finally {
            if (statement!=null) {
                try {
                    statement.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
            if (connection!=null) {
                try {
                    connection.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        }

    }

B程序:

public static void main(String[] args) {

    Connection connection=null;
    Statement statement=null;
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=GMT", "root", "123456");
        connection.setAutoCommit(false);
        statement=connection.createStatement();
        //B账户加钱
        int num=statement.executeUpdate("update account set balance_B=balance_B+1000 where id=1");
        System.out.println(num==1?"转账成功":"转账失败");

        //提交事务
        connection.commit();

    } catch (Exception e) {

        //事务回滚
        try {
            connection.rollback();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }

        e.printStackTrace();


    }finally {
        if (statement!=null) {
            try {
                statement.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        if (connection!=null) {
            try {
                connection.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }
}

我们在程序A的connection.commit();这一行设置一个断点。Debug A程序,此时断点处事务并未提交,因此,写锁并未释放。然后,我们run B程序,准备对account表中的同一行做修改。

可以发现B程序一直阻塞住,并没有执行成功。

现在我们释放A程序中的断点,即提交事务A。再次查看B程序

可以看到转账成功,因为事务A提交,释放了写锁,那么其他的进程就可以读取该行数据,所以显示转账成功。

PreparedStatement防止SQL注入

SQL注入:

​ 指的就是通过在SQL中加入数据库的关键字,导致最终SQL执行的结果是错误的

select * from user where username="张三1" and password="1231";
select * from user where 1 = 1 or password="";

PreparedStatement

​ Statement对象没有预编译过程,直接执行SQL。
​ select * from user where username=张三1 and password=1231;

​ PreparedStatement是Statement的子类,使用这个类对于的对象可以防止SQL注入,因为这个对象在执行SQL时候
分为3个步骤:

  1. 先预编译SQL,搭建SQL语句框架,但是并没有赋值

    先用占位符代替数据

    select * from user where username=? and password=? ;

    //获取数据库操作对象并且预编译SQL
    preparedStatement=connection.prepareStatement("select * from user where username=? and password=?");
    
  2. 给SQL中注入具体的数据

    preparedStatement.setString(1,"张三1");
    preparedStatement.setString(2,"1231");
    

    即?=张三1 ?=1231

  3. 执行SQL

    resultSet=preparedStatement.executeQuery();
    

封装JDBC工具类

  1. 构造方法私有化
  2. 所有的方法都要是静态方法
import java.sql.*;

/**
 * 封装JDBC工具类
 */

public class JDBCUtils {

    //构造方法私有化
    private JDBCUtils()
    {

    }
    //静态代码块做类加载加载驱动
    static {
        try {
            //注册驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 用于获取数据库对象
     */
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/test" +
                "?serverTimezone=GMT&characterEncoding=utf-8", "root", "123456");
    }

    /**
     * 用于关闭资源
     */
    public static void getClose(ResultSet resultSet, Statement statement,Connection connection){

        if (resultSet!=null) {
            try {
                resultSet.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        if (statement!=null) {
            try {
                statement.close();
            } catch (SQLException throwables) {


            }
        }
        if (connection!=null) {
            try {
                connection.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }
}

测试:

public static void main(String[] args) {
    Connection connection =null;
    PreparedStatement preparedStatement =null;
    ResultSet resultSet =null;
    try {
        //获取数据库连接 驱动加载在静态代码块中已经执行
        connection = JDBCUtils.getConnection();
        //获取数据库操作对象
        preparedStatement = connection.prepareStatement("select * from user");
        //执行SQL
        resultSet = preparedStatement.executeQuery();

        if (resultSet!=null)
        {
            while (resultSet.next())
            {
                int uid = resultSet.getInt("uid");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                System.out.println(uid+" "+username+" "+password);
            }
        }

    } catch (Exception throwables) {
        throwables.printStackTrace();
    }finally {
        JDBCUtils.getClose(resultSet,preparedStatement,connection);
    }

}
posted @ 2021-07-29 17:26  ins1mnia  阅读(34)  评论(0编辑  收藏  举报