Day21- JDBC事务(JDBC加强)

本文深入解析MySQL中的事务概念,包括默认事务、手动事务的管理方式,以及如何通过Java JDBC实现转账案例,并探讨了事务的四大特性:原子性、一致性、隔离性和持久性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

事物:

 事物指的是逻辑上的一组操作,组成这组操作的各个单元要么全部成功,要么全部失败
 简而言之,一组操作要么都成功,要么都失败; 一件事情有n个组成单元 要不这n个组成单元同时成功 要不n个单元就同时失败。

mysql的事务
1)默认的事务:
一条sql语句就是一个事务,默认就开启事务并提交事务
MySql默认自动提交,即执行一条sql语句就提交一次事务;
2)手动事务:
1)显示的开启一个事务:start transaction;
2)事务提交:commit代表从开启事务到事务提交 中间的所有的sql都认为有效 真正的更新数据库
3)事务的回滚:rollback 代表事务的回滚 从开启事务到事务回滚 中间的所有的 sql操作都认为无效数据库没有被更新
这里写图片描述

MySql中可以有两种方式对事务进行管理:
方式一:关闭mysql的自动事务

     set autocommit = 0;

注意:这个设置只对当前的连接有效,新开的窗口,依然是自动事务

方式二:手动开启一个事务:

start transaction; -- 手动开启一个事务
-- 假装做了一系列的sql操作
commit; -- 提交事务 : 相当于把数据持久保存到数据据库中
rollback; -- 回滚事务 : 相当于撤销刚才所有的操作

注意:
1)事务开启之后,最终必须提交或者回滚
2)oracle中的默认是手动事务,mysql默认是自动事务
3)控制事务的connnection必须是同一个
执行sql的connection与开启事务的connnection必须是同一个才能对事务进行控制


jdbc中的事务操作:

Connection接口的API(jdk api中搜connection )
- setAutoCommit(false) : 设置默认不自动提交事务
- commit(); 提交事务。将数据持久保存在数据库中
- rollback(); 事务的回滚。撤销事物中的操作
- rollback(savepoint sp); 事务的回滚至保存点


转账案例:

 事务的控制需要在同一个connection之中,所以下述案例中统一传了一个对象connection,保证是在同一个连接之中。
package com.itheima.test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.junit.Test;
import com.itheima.domain.Transfer;

public class Test01 {

      @Test
      public void test01() throws Exception{
           //注册驱动
           Class.forName("com.mysql.jdbc.Driver");
           //连接数据库
           String url = "jdbc:mysql://localhost:3306/day21";
           String user = "root";
           String password = "123456";
           Connection connection = DriverManager.getConnection(url, user, password);

           //关闭自动事务
           connection.setAutoCommit(false);

           //封装一个javaBean
           Transfer transfer = new Transfer();
           transfer.setFromUser("jack");
           transfer.setToUser("rose");
           transfer.setMoney(500);

           try {
                 //转出钱
                 transferOut(connection, transfer);
                 //如果发生意外
                 //int i = 5/0;
                 //转入钱
                 transferIn(connection, transfer);
                 //提交事务
                 connection.commit();
           } catch (Exception e) {
                 e.printStackTrace();
                 //事务回滚
                 connection.rollback();
                 System.out.println("转账失败");
           }finally{
                 //关闭资源 
                 connection.close();
           }
      }

      /**
       * @Title: Test01.java
       * @Description: TODO(资金从转账人账户转出)
       * @param connection
       * @author jjizh
       * @date 2017年6月30日 下午8:33:00
       * @version V1.0
       * @throws SQLException
       */
      public void transferOut(Connection connection,Transfer transfer) throws SQLException{
           //获得转出金额和从谁转出
           double money = transfer.getMoney();
           String name = transfer.getFromUser();
           //创建执行sql语句的对象
           String sql = "update user set money = money - ? where name = ? ";
           PreparedStatement prepareStatement = connection.prepareStatement(sql);
           prepareStatement.setDouble(1, money);
           prepareStatement.setString(2, name);
           //执行sql语句
           prepareStatement.executeUpdate();
           //关闭资源
           prepareStatement.close();
      }

      /**
       * @Title: Test01.java
       * @Description: TODO(资金转入给收款人)
       * @param connection
       * @param transfer
       * @throws SQLException
       * @author jjizh
       * @date 2017年6月30日 下午8:48:22
       * @version V1.0
       */
      public void transferIn(Connection connection,Transfer transfer) throws SQLException{
           //获得转入金额和转入给谁
           double money = transfer.getMoney();
           String name = transfer.getToUser();
           //创建执行sql语句的对象
           String sql = "update user set money = money + ? where name = ? ";
           PreparedStatement prepareStatement = connection.prepareStatement(sql);
           prepareStatement.setDouble(1, money);
           prepareStatement.setString(2, name);
           //执行sql语句
           prepareStatement.executeUpdate();
           //关闭资源
           prepareStatement.close();
      }
}

DBUtils事务操作

DBUtils中的api

* commitAndClose(Connection conn);
* commitAndCloseQuietly(Connection conn); 不抛异常
* rollbackAndClose(Connction conn);
* rollbackAndCloseQuietly(Connection conn);不抛异常

queryRunner操作sql:
1)new QueryRunner(DataSource ds): 默认就是自动事务(这是我们之前一直用的方法)
query(String sql,ResultSetHandler rsh,Object…params);
update(String sql,Object…params);
使用上述方法的时候,我们没有调用过setAutoCommit(false),最后也没有提交过事务,也没有关闭资源

2)new QueryRunner() : 手动事务
query(Connection conn,String sql,ResultSetHandler rsh,Object…params);
update(Connection conn,String sql,Object…params);
使用query和update方法的时候,我们需要传入一个connection对象,最后需要手动提交或者回滚事务,还需要手动释放conn;


案例补充知识点:

1)主动抛异常

throw new Exception("")
if(!flag2){
      throw new RuntimeException("对不起,转入失败");
}

2)获取异常中的抛出信息

Exception.getMessage();
catch (Exception e) {
                 e.printStackTrace();
                 request.setAttribute("msg", e.getMessage());
}

代码:
Transferservlet:

public class TransferServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");

        try {
            //创建转账对象
            Transfer t = new Transfer();
            //接收参数
            Map<String, String[]> map = request.getParameterMap();
            //将参数传入到对象中去
            BeanUtils.populate(t, map);

            //调用service完成转账
            TransferService ts = new TransferService();

            ts.transfer(t);

            request.setAttribute("msg", "转账成功");
            request.setAttribute("transfer", t);
            request.getRequestDispatcher("/index.jsp").forward(request, response);

        } catch (Exception e) {
            e.printStackTrace();
            request.setAttribute("msg", e.getMessage());
            request.getRequestDispatcher("/index.jsp").forward(request, response);
        }
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

TransferService:

public class TransferService {

    public void transfer(Transfer t) throws Exception {
        Connection connection = null;
        try {
            //获得连接
            connection = JDBCUtils.getConnection();
            //开启手动事务
            connection.setAutoCommit(false);

            TransferDao td = new TransferDao();
            //转出
            boolean flag1 = td.moneyOut(connection,t);
            if(!flag1){
                throw new RuntimeException("对不起,转出失败");
            }

            //转入
            boolean flag2 = td.moneyIn(connection,t);
            if(!flag2){
                throw new RuntimeException("对不起,转入失败");
            }
            //提交commit
            connection.commit();
        } catch (Exception e) {
            JDBCUtils.rollbackQietly(connection);
            throw e;
        } finally{
            //释放资源
            JDBCUtils.closeQuietly(connection);
        }
    }

}

转账案例终极版:

ThreadLocal
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
例如:

private static ThreadLocal tl = new ThreadLocal<T>();

private static ThreadLocal tl = new ThreadLocal<Connection>();
 threadLocal相当于一个map,以当前线程的id作为一个key,对应存入一个value

api中:
set(T value)直接传入一个value,设置一个值
get():不需要传入任何参数,key就是当前线程
remove():不需要传入任何一个参数, key就是当前线程
这里写图片描述

所以转账的优化流程可以优化成这个样子:
这里写图片描述
所以:
service层的代码优化成为:
TransferService_thrl:

public class TransferService_thrl {

      public void transfer(Transfer t) throws Exception {

           try {
                 //开启手动事物
                 JDBCUtils.startTransaction();

                 TransferDao_thrl td = new TransferDao_thrl();
                 //转出
                 boolean flag1 = td.moneyOut(t);
                 if(!flag1){
                      throw new RuntimeException("转出失败!!!!");
                 }
                 //转入
                 boolean flag2 = td.moneyIn(t);
                 if(!flag2){
                      throw new RuntimeException("转入失败!!!!");
                 }
                 //提交事务
                 JDBCUtils.commitAndClose();
           } catch (Exception e) {
                 JDBCUtils.rollbackAndClose();
                 e.printStackTrace();
           }

      }
}

JDBCUtils中的函数优化为:

public class JDBCUtils {

    private static DataSource ds = new ComboPooledDataSource();

    private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    public static DataSource getDataSource(){
        return ds;
    }

    public static Connection getConnection() throws SQLException{
        //从当前线程中去获取connection;
        Connection connection = tl.get();
        if(connection == null){
            connection = ds.getConnection();
            tl.set(connection);
        }

        return connection;
    }

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

    //提交事务
    public static void commitAndClose() {
        try {
            Connection connection = getConnection();
            connection.commit();
            connection.close();
        } catch (SQLException e) {
            // quiet
//            e.printStackTrace();
        }
    }

    public static void rollbackAndClose()  {
        try {
            Connection connection = getConnection();
            connection.rollback();
            connection.close();
        } catch (SQLException e) {
            // quiet
//            e.printStackTrace();
        }
    }
}

备注:servlet中的service每次是在一个新的线程中去执行的


事务特性ACID

1)原子性:Atomicity
- 事务不可切分,事务中可以包含多个操作,这些操作要么都成功,要么都失败
2)一致性:Consistency
- 事务执行前后的业务状态要和其它业务状态保持一致
3)隔离性:isolation
- 一个事务的执行,不受其它事务的影响
4)持久性:Durability
- 事务一旦提交,数据就持久保存到数据库中

并发访问问题:
如果不考虑隔离性,事务存在3种并发访问问题:
- 脏读: 一个事务读取到了另一个事务没有提交的数据
- 不可重复读:在一个事务中,两次查询的结果不一致(针对update)
- 虚读(幻读):在一个事务中,两次查询的结果不一致(针对insert)

设置隔离级别,避免问题产生
- read uncommitted 读未提交
- 上面所有的问题全都不可避免
- read committed 读取已提交
- 可以避免脏读的发生,不可重复读和幻读会发生
- repeatable read 可重复读
- 可以避免脏读和不可重复读的发生,不能避免幻读的发生
- serializable 串行化
- 可以避免所有的问题,一个事务在执行,另外一个事务等待

  • 安全性: serializable > repeatable read > read committed > read uncommitted
  • 效率刚好和上面的相反

  • mysql的默认隔离级别: repeatable read

  • oracle的默认隔离级别: read commttied – read only – serializable

  • java代码修改数据库的隔离级别(了解):

    • conn.setTransactionIsolation(int level)

MySql中:
1)将数据库的隔离级别设为read uncommitted
set session transaction isolation level read uncommitted;

2)查看隔离级别
select @@tx_isolation;

3)将数据库的隔离级别设为read committed
set session transaction isolation level read committed;
不能避免不可重复读和幻读的发生

4)将数据库的隔离级别设为repeatable read
set session transaction isolation level repeatable read;

5)将数据库的隔离级别设为serializable
set session transaction isolation level serializable;


jsp中输出菱形

在控制台中输出:

      @Test
      public void test01(){
           int size = 10;
           for(int x=-size;x<=size;x++){
                 for(int y=-size;y<=size;y++){
                      if(y+x<=size && y+x>=-size && y-x<=size && y-x>=-size ){
                            System.out.print("*");
                      }else{
                            System.out.print(" ");
                      }
                 }
                 System.out.println();
           }

      }
}

在jsp页面输出
因为jsp页面中使用jstl,循环是只能从0开始,不能够从负数开始的,所以需要手工调整坐标系位置

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>输出菱形</title>
</head>
<body>
      <c:forEach var="x" begin="0" end="5" step="1" varStatus="xStatus">
           <c:forEach var="y" begin="0" end="5" step="1" varStatus="yStatus">
                 <c:choose>
                      <c:when test="${x+y<=6 && x+y>=2 && x-y<=2 && x-y>=-2}">
                            *
                      </c:when>
                      <c:otherwise>
                            &nbsp;
                      </c:otherwise>
                 </c:choose>
           </c:forEach>
           <br/>
      </c:forEach>
</body>
</html>

JSP三层架构
包名:
这里写图片描述