JDBC_事务管理

一、什么是事务

逻辑上的一组操作, ,要不全部失败,要不全部成功。

对数据库的操作会先在一张临时表上进行,一旦发生错误,就回滚撤销所有操作。全部成功才存盘修改数据库数据。

MySql的事务管理

1. 启动事务(对数据的操作在临时表中进行)
    startTransaction

2. 提交,存盘
    commit

3. 回滚,撤销
    rollBack
  • 在事务管理中执行sql,使用数据库内临时表保存,在没有进行事务提交或者回滚之前,其它用户无法看到事务操作的结果
  • SQL语言中只有DML才能被事务管理(insert/update/delete)

JDBC的事务

1. 启动事务管理
    //首先获取连接。(以C3P0为基础准备一个JDBCUtils工具类,提供获取连接池和连接的方法)
    Connection connection = JDBCUtils.getConnection();
    //启动事务
    connection.setAutoCommit(false);

2. 提交,存盘
    connection.commit();

3. 回滚,撤销
    connection.rollBack();

DBUtils的事务管理

1. 启动事务
    //创建QuueryRunner对象,使用它的无参构造方式,自己给连接。从而可以通过连接控制事务
    QueryRunner runner = new QueryRunner();
    Connection connection = JDBCUtils.getConnection();
    connection.setAutoCommit(false);

2. 提交并释放资源,异常由框架处理(其实没处理)
    //connection.commitAndCloseQuietly();

3. 回滚,撤销并释放资源,异常由框架处理(其实没处理)
    //connection.rollbackAndCloseQuietly();

事务回滚点 SavePoint(类似与游戏中的存档)

当事务特别复杂,有些情况不会回滚到事务的最开始状态,需要将事务回滚到指定位置

* 核心API
    * connection.setSavepoint();// 设置回滚点
    * connection.rollback(savepoint);//事务回滚到指定回滚点
  • 示例代码
    • 往数据库中添加数据,每1000条保存一次。

    Connection connection = null;
    PreparedStatement prepareStatement = null;
    Savepoint savepoint = null;
    try {
        connection = JDBCUtils.getConnection();
        // 开启事务
        connection.setAutoCommit(false);
        // 设置初始回滚点
        savepoint = connection.setSavepoint();
        String sql = "insert into person values (?,?)";
        prepareStatement = connection.prepareStatement(sql);
        for (int i = 1; i <= 5000; i++) {
            prepareStatement.setInt(1, i);
            prepareStatement.setString(2, "name" + i);
            prepareStatement.addBatch();
            // 模拟错误
            if (i == 3201) {
                int x = 11 / 0;
            }
            // 每隔200条数据执行一次批处理
            if (i % 200 == 0) {
                prepareStatement.executeBatch();
                prepareStatement.clearBatch();
            }
            if (i % 1000 == 0) {
                // 每一千条数据,创建一个回滚点
                savepoint = connection.setSavepoint();
            }
        }
        prepareStatement.executeBatch();
        prepareStatement.clearBatch();
        // 没有异常,提交事务
        connection.commit();
    } catch (Exception e) {
        try {
            // 发生异常,事务回滚到指定回滚点
            connection.rollback(savepoint);
            // 提交事务
            connection.commit();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
    } finally {
        JDBCUtils.release(connection, prepareStatement);
    }

二、事务案例:简易转账案例

1、分析

JDBC_事务管理_第1张图片
img01.png

2、搭建环境

1. 数据库及用户表单的创建。

    CREATE DATABASE day20;
    USE day20;
    CREATE TABLE USER(
    id INT PRIMARY KEY AUTO_INCREMENT,
    NAME VARCHAR(40),
    money DOUBLE
    );
    INSERT INTO USER VALUES(NULL,'tom',2000);
    INSERT INTO USER VALUES(NULL,'jerry',2000);

2. 数据库驱动,c3p0的jar包,c3p0.xml配置文件,dbutils的jar包,

3. 转账页面
    
转账人:
被转账人:
转账金额:

3、分包实现

3.1、创建web层TransactServlet

Servlet要做的三件事:

  1. 获取表单提交的参数
  2. 调用业务逻辑
  3. 分发转向

    public class TransferServlet extends HttpServlet {
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            // 设置编码集
            response.setContentType("text/html;charset=UTF-8");
    
            try {
                // 获取参数
                String sender = request.getParameter("sender");
                String receiver = request.getParameter("receiver");
                String amount = request.getParameter("amount");
                
                // 调用业务层对象,执行转账
                TransferService service = new TransferService();
                service.transfer(sender, receiver, amount);
                // 写出响应
                response.getWriter().write("转账成功");
            } catch (Exception e) {
                // 写出响应
                response.getWriter().write("转账失败:" + e.getMessage());
            }
    
        }
    
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            doGet(request, response);
        }
}

3.2、创建service层TransferService

与数据库交互的操作交到dao层完成,service层只处理业务逻辑


    public class TransferService {
    
        public void transfer(String sender, String receiver, String amount) throws Exception {
            Connection connection = null;
    
            try {
                // 与数据库交互的事情交给dao层去操作 ,创建dao对象
                TransferDao dao = new TransferDao();
    
                // 开启事务。要想控制事务dao层的connection连接对象和这里的连接对象应该一致.因此将这个connection对象作为参数传递下去。
                //connection = JDBCUtils.getConnection();
                //connection.setAutoCommit(false);
                //为了不打破分层结构,不应该将连接Connection的操作放在Service层.因此抽取出来
                JDBCUtils.startTransaction();
    
                // 改变的行数
                int outDao = dao.outDao(sender, amount);
                
                //一旦数据库操作失败,影响行数即为0,抛出异常带上对应的信息。
                if(outDao != 1) {
                    throw new RuntimeException("转出失败");
                }
                
                int inDao = dao.inDao(receiver, amount);
                
                if(inDao != 1) {
                    throw new RuntimeException("转入失败");
                }
    
                // 没有异常,提交事务
                JDBCUtils.commitAndRelease();

            //这里是用try-catch是为了保证有异常与无异常时的两种处理方式
            //这里抓住了上面抛出的转账信息的异常但是为了告诉Servlet这里发生了异常,将该异常继续向上抛
            } catch (Exception e) { 

                // 发生异常,回滚撤销操作
                JDBCUtils.rollbackAndRelease();
                throw e;
            }
    
        }
    }

3.3、创建utils工具类JDBCUtils

提供与数据库操作相关的工具:获取连接池与获取连接的方法

因为DBUtils框架的QueryRunner对象会自行释放资源,因此不提供释放Connection等资源的方法。

private static DataSource dataSource = new ComboPooledDataSource();

    // 获取连接池对象
    public static DataSource getDataSource() {
        return dataSource;
    }

    // 提供连接
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

3.4、创建dao层TransferDao

进行操作数据库的动作。


    public class TransferDao {
    
        public int outDao(String sender, String amount) throws SQLException {
            //对数据库数据进行更改
            //1.创建QueryRunner对象
            QueryRunner runner = new QueryRunner();
            Connection connection = JDBCUtils.getConnectionTL();
            //2.执行sql语句
            String sql = "update user set money=money-? where name=?";

            //为了告知service层sql语句是否执行成功,返回被影响的行数
            int i = runner.update(connection, sql, amount,sender);
            return i;
        }
    
        public int inDao(String receiver, String amount) throws SQLException {
            //对数据库数据进行更改
            //1.创建QueryRunner对象
            QueryRunner runner = new QueryRunner(JDBCUtils.getDataSource());
            Connection connection = JDBCUtils.getConnectionTL();
            //2.执行sql语句
            String sql = "update user set money=money+? where name=?";
            int i = runner.update(connection, sql, amount,receiver);
            return i;
        }
    }

3.5、优化代码,完善utils工具类

  1. 因为第一版中service层执行了属于dao层与数据库交互的操作,这打破了三层结构,为了避免这种情况,应当考虑将与数据库交互的相关操作抽取出来,于是我将控制事务的代码抽取到工具类中。提供开启事务,提交事务和回滚事务的方法。

  2. 但是为了控制事务,service层和dao层的connection应当是同一个connection,因此使用了一个类似于map集合的ThreadLocal类,它的内部存在的键值对,通过它保证获取同一个connection连接。


    public class JDBCUtils {
        private static DataSource dataSource = new ComboPooledDataSource();
        private static ThreadLocal threadLocal = new ThreadLocal();
    
        // 获取连接池对象
        public static DataSource getDataSource() {
            return dataSource;
        }
    
        // 提供连接
        public static Connection getConnection() throws SQLException {
            return dataSource.getConnection();
        }
    
        // 提供连接,从ThreadLocal中获取的
        public static Connection getConnectionTL() throws SQLException {
            // 从ThreadLocal中获取连接
            Connection connection = threadLocal.get();
            // 如果连接对象为空
            if (connection == null) {
                // 从连接池获取连接
                connection = getConnection();
                // 将连接放入ThreadLocal,这样下一次从ThreadLocal中获取的时候就有数据了
                threadLocal.set(connection);
            }
    
            return connection;
        }
    
        // 开启事务
        public static void startTransaction() throws SQLException {
            Connection connection = getConnectionTL();
            connection.setAutoCommit(false);
    
        }
    
        // 提交事务并释放资源
        public static void commitAndRelease() throws SQLException {
            Connection connection = getConnectionTL();
            // 提交事务
            connection.commit();
            if (connection != null) {
                connection.close();
                connection = null;
            }
            // 把连接对象和ThreadLocal解绑,可以让ThreadLocal尽快被释放,节约服务器内存资源
            threadLocal.remove();
    
        }
    
        // 回滚事务并释放资源
        public static void rollbackAndRelease() throws SQLException {
            Connection connection = getConnectionTL();
            // 回滚事务
            connection.rollback();   
            if (connection != null) {   
                connection.close();
                connection = null;
            }
            // 把连接对象和ThreadLocal解绑,可以让ThreadLocal尽快被释放,节约服务器内存资源
            threadLocal.remove();
    
        }
    
    }

3.6、 案例实现_为什么要使用事务?

  • 如果不考虑事务,会导致转账钱丢失的问题.所以必须考虑事务

  • 如果要避免上述问题,只需要保证所有的操作使用同一个连接对象即可

  • 解决方法

    • 传递参数Connection(打破了三层架构,因此采用线程绑定)
    • 线程绑定ThreadLocal
  • ThreadLocal简介

    • 作用 : 把一个操作对象和当前线程绑定在一起. 其内部维护了一个Map集合.key就是当前线程,value就是要绑定的内容
    • 常用API
      • set(T value) : 把一个对象和当前线程进行绑定.等价于Map.put(Thread.currentThread(),value)
      • T get() : 获取和当前线程绑定在一起的对象.等价于Map.get(Thread.currentThread())
      • remove() : 移除和当前线程绑定在一起的对象.等价于Map.remove(Thread.currentThread())
JDBC_事务管理_第2张图片
![img03.png](http://upload-images.jianshu.io/upload_images/5303154-013edb1268a6c647.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

4、事务的一些概念

事务特性

  • 事务的四大特性:

    • 原子性(Atomicity):事务的一组操作不可分割,要么都成功,要么都失败
    • 一致性(Consistency):事务前后数据保持完整性.转账前A和B账户总和2000元,转账后总和还是2000 元
    • 隔离性(Isolation):并发访问时,事务之间是隔离的,一个事务不应该影响其它事务的运行效果
    • 持久性(Durability):当事务一旦提交,事务数据永久存在,无法改变
  • 企业开发中一定要保证事务原子性

  • 事务最复杂问题都是由事务隔离性引起的

隔离性

  • 不考虑事务隔离将引发的问题

    • 脏读:一个事务读取另一个事务未提交的数据.这是数据库隔离中最重要的问题
    • 不可重复读:一个事务读取另一个事务已提交的数据,在一个事务中两次查询结果不同(针对update操作)
    • 虚读:一个事务读取另一个事务插入的数据,造成在一个事务中两次查询记录条数不同(针对insert操作)
  • 数据库为了解决三类隔离引发问题,提供了四个数据库隔离级别(所有数据库通用)

    • Serializable : 串行处理.可以解决三类问题
    • Repeatable read :可以解决不可重复读、脏读,但是会发生虚读.是MySQL的默认级别
    • read committed : 可以解决脏读,会发生不可重复读、虚读.是Oracle的默认级别
    • read uncommitted : 会导致三类问题发生
    • 按照隔离级别从高到低排序 : Serializable > Repeatable read > read committed > read uncommitted
    • 数据库隔离问题危害的排序 : 脏读> 不可重复读 > 虚读
    • 多数数据库厂商都会采用Repeatable read或read committed两个级别.
  • 更改事务隔离级别的语句

    • set transaction isolation level 设置事务隔离级别
    • select @@tx_isolation; 查询当前事务隔离级别

隔离级别引发问题的小实验

  • 脏读问题(read uncommitted)

    • 开启两个窗口,执行一次查询,获得一个结果
    • 将B窗口隔离级别设置为read uncommitted
      • set session transaction isolation level read uncommitted;
    • 在A、B窗口分别开启一个事务 start transaction;
    • 在A窗口完成转账操作
      • update account set money= money - 200 where name='aaa';
      • update account set money= money +200 where name='bbb';
    • 在B窗口进行查询,会读取到A窗口未提交的转账结果
    • A窗口进行回滚rollback, B窗口查询结果恢复之前
  • 不可重复读(read committed)

    • 开启两个窗口,执行一次查询,获得一个结果
    • 将B窗口隔离级别设置为read committed
      • set session transaction isolation level read committed;
    • 在A、B窗口分别开启一个事务 start transaction;
    • 在A窗口完成转账操作
      • update account set money= money - 200 where name='aaa';
      • update account set money= money +200 where name='bbb';
    • 此时在B窗口执行查询操作,数据不会发生改变.避免了脏读问题
    • A窗口执行commit,B窗口再次执行查询,会读取到A窗口提交的结果.注意此时B窗口没有提交事务,也就是在同一事务中,读取到了两个结果.发生不可重复读问题
  • 虚读(Repeatable read)

    • 开启两个窗口,执行一次查询,获得一个结果
    • 将B窗口隔离级别设置为Repeatable read
      • set session transaction isolation level repeatable read;
    • 在A、B窗口分别开启一个事务 start transaction;
    • 在A窗口完成转账操作
      • update account set money= money - 200 where name='aaa';
      • update account set money= money +200 where name='bbb';
    • 此时在B窗口执行查询操作,数据不会发生改变.避免了脏读问题
    • A窗口执行commit,B窗口再次执行查询,数据仍然不会发生改变.避免了不可重复读.
    • 此时如果在A窗口插入一条数据,而B窗口可以查询到,就是发生了虚读问题.但是这种情况发生的几率非常小.
  • Serializable

    • 开启两个窗口,执行一次查询,获得一个结果
    • 将B窗口隔离级别设置为read serializable
      • set session transaction isolation level serializable;
    • 在A、B窗口分别开启一个事务 start transaction;
    • 在B窗口执行查询操作
    • 在A窗口执行插入操作.此时A窗口将会被卡住,不会执行语句.直到B窗口提交或回滚,释放数据库资源
  • 在JDBC中,可以通过Connection.setTransactionIsolation(int level) 来设置隔离级别.如果没有设置.会采用数据库的默认级别

丢失更新问题和悲观锁乐观锁机制【了解】

  • 事务丢失更新问题 : 两个事务同时读取同一条记录,A先修改记录,B也修改记录(B不知道A修改过),B提交数据后B的修改结果覆盖了A的修改结果。

  • 解决丢失更新的两种方式

    • 事务和锁是不可分开的,锁一定是在事务中使用 ,当事务关闭锁自动释放
    • 悲观锁
      • 假设丢失更新会发生
      • 使用数据库内部锁机制,进行表的锁定,在A修改数据时,A就将数据锁定,B此时无法进行修改
      • 在mysql中默认情况下,当你修改数据,自动为数据加锁(在事务中),防止两个事务同时修改数据
      • 在mysql内部有两种常用锁
        • 读锁(共享锁)
          • 一张表可以添加多个读锁,如果表被添加了读锁(不是当前事务添加的),该表不可以修改
          • 语法 : select * from account lock in share mode;
          • 共享锁非常容易发生死锁
        • 写锁(排它锁)
          • 一张表只能加一个排它锁,排他锁和其它共享锁、排它锁都具有互斥效果 。
          • 如果一张表想添加排它锁,前提是之前表一定没有加过共享锁和排他锁
          • 语法 : select * from account for update ;
    • 乐观锁
      • 假设丢失更新不会发生
      • 使用的不是数据库锁机制,而是一个特殊标记字段 : 数据库timestamp 时间戳字段

你可能感兴趣的:(JDBC_事务管理)