# 事务

现阶段,要进行事务控制,前提是所有的数据操作基于同一个Connetion对象

connection.setAutoCommit(false) 开启事务 start transaction ;

connection.commit() 提交事务 commit

connection.rollback(); 回滚事务 rollback

事务控制要放在service层实现类

用ThreadLocal解决多线程下,事务控制的问题:

1 在工具类中定义一个私有静态的ThreadLocal对象,指明变量类型是Connection;

2 在自定义的开启事务的方法中,先从ThreadLocal获取Conneciton,如果没有从连接池

中获取一个连接,放到ThreadLocal中,并开始事务

3在自定义的提交事务方法中,先从ThreadLocal获取Conneciton,提交事务,将连接归还到

连接池中,从ThreadLocal移除此连接

4在自定义的回滚事务方法中,先从ThreadLocal获取Conneciton,回滚事务,将连接归还到

连接池中,从ThreadLocal移除此连接

写在后面:此处总结不是为了面试,主要是梳理编程思路,在真实项目中,ThreadLocal用的最多的场景是

保存登录用户的信息。

# 一、准备工作

一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。

事务要处理的问题,把多个对数据库的操作绑定成一个事务,要么都成功,要么都失败。

DROP DATABASE if exists mydb2;
CREATE DATABASE mydb2;

USE mydb2;


DROP TABLE IF EXISTS account;
CREATE TABLE account  (
  id int(11) NOT NULL AUTO_INCREMENT,
  cardnum varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  money decimal(10, 0) NULL DEFAULT NULL,
  PRIMARY KEY (id) USING BTREE
);

INSERT INTO account VALUES (1, '10001', 5000);
INSERT INTO account VALUES (2, '10002', 5000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 二、JDBC中处理事务

在JDBC中处理事务,都是通过Connection完成的;

同一事务中所有的操作,必须使用同一个Connection对象。

2.1、2.5必须掌握,2.2~2.4了解分析过程就可以。

# 2.1、相关方法(必须掌握)

以下的方法都是Connection中的方法。

setAutoCommit(boolean)如果true(默认值就是true)表示自动提交,con.setAutoCommit(false)表示开启事务;

commit()提交事务

rollback()回滚事务

JDBC处理事务的代码格式:

try {
  con.setAutoCommit(false);//开启事务…      mysql中开启是start transaction
  //多个JDBC操作
  
  con.commit();//try的最后提交事务            mysql 中提交 commit          
} catch() {
  con.rollback();//回滚事务                  mysql rollback
}
1
2
3
4
5
6
7
8

# 2.2、Dao层处理事务

public class AccountDao {    
	//转账
    /*
     * src:源卡号
     * dst:目的卡号
     * */
    /*
     * 使用传统的JDBC方式进行转账操作
     * */
    public void trans(String src, String dst, double money) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        String sql1 = "update account set money=money-? where cardNum=?";
        String sql2 = "update account set money=money+? where cardNum=?";
        try {
            //获取连接
            conn = JdbcUtils.getConnection();
            //开启事务
            conn.setAutoCommit(false);//禁止自动提交 即开启事务 
            //转账操作
            //操作1
            pstmt = conn.prepareStatement(sql1);
            //设置参数
            pstmt.setDouble(1, money);
            pstmt.setString(2, src);
            //发送SQL
            pstmt.executeUpdate();

            //用来测试回滚
            //int i = 100/0;

            //操作2
            pstmt = conn.prepareStatement(sql2);
            //设置参数
            pstmt.setDouble(1, money);
            pstmt.setString(2, dst);
            //发送SQL
            pstmt.executeUpdate();

            //提交事务
            conn.commit();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                //回滚事务
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        } finally {
            //释放资源
			JdbcUtils.close(conn, null, null);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 2.3、Service层才是处理事务的地方

我们要清楚一件事,DAO中不是处理事务的地方,因为DAO中的每个方法都是对数据库的一次操作,而Service中的方法才是对应一个业务逻辑。也就是说我们需要在Service中的一方法中调用DAO的多个方法,而这些方法应该在一个事务中。

DAO层中的方法只进行最细粒度的增删改查;

Service层处理业务,对DAO层的方法进行组合。

怎么才能让DAO的多个方法使用相同的Connection呢?方法不能再自己来获得Connection,而是由外界传递进去。

public void daoMethod1(Connection con,) {//操作1 减去xx
}

public void daoMethod2(Connection con,) {//操作2 加上xx
}
1
2
3
4
5

# 2.3.1、DAO层代码

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {  
  /*
     * 提款withdrawal
     * */
    public void withdrawal(Connection conn, String cardNum, double money) throws SQLException {
        String sql = "update account set money=money-? where cardNum=?";
        PreparedStatement pstmt = null;

        //操作
        pstmt = conn.prepareStatement(sql);
        //设置参数
        pstmt.setDouble(1, money);
        pstmt.setString(2, cardNum);
        //发送SQL
        pstmt.executeUpdate();

        pstmt.close();
    }

    /*
     * 存款deposit
     * */
    public void deposit(Connection conn, String cardNum, double money) throws SQLException {
        String sql = "update account set money=money+? where cardNum=?";

        PreparedStatement pstmt = null;

        //操作
        pstmt = conn.prepareStatement(sql);
        //设置参数
        pstmt.setDouble(1, money);
        pstmt.setString(2, cardNum);
        //发送SQL
        pstmt.executeUpdate();

        pstmt.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 2.3.2、Service层代码

import com.qfedu.dao.AccountDao;
import com.qfedu.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.SQLException;

public class AccountService {
    private AccountDao accountDao = new AccountDao();
    //转账操作
    public void trans(String src, String dst, double money) {
        Connection connection = null;

        try {
            //获取连接
            connection = JdbcUtils.getConnection();
            //开启事务
            connection.setAutoCommit(false);
            //提款
            accountDao.withdrawal(connection, src, money);
            
            //用来测试回滚
            int i = 100/0;
            
            //存款
            accountDao.deposit(connection, dst, money);
            //提交事务
            connection.commit();
        } catch (Exception e) { //注意这里异常的类型
            e.printStackTrace();
            try {
                //回滚
                connection.rollback();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        } finally {
            //释放资源
            JdbcUtils.close(connection, null, null);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

在Service中不应该出现Connection,它应该只在DAO中出现,因为它是JDBC的东西,JDBC的东西是用来连接数据库的,连接数据库是DAO的事情,但是,事务是Service的事儿,不能放到DAO中。

# 2.4、修改JdbcUtils

我们把对事务的开启和关闭放到JdbcUtils中,在Service中调用JdbcUtils的方法来完成事务的处理,但在Service中就不会再出现Connection这一“禁忌”了。

DAO中的方法不用再让Service来传递Connection了。DAO会主动从JdbcUtils中获取Connection对象,这样,JdbcUtils成为了DAO和Service的中介!

我们在JdbcUtils中添加beginTransaction()和rollbackTransaction(),以及commitTransaction()方法。这样在Service中的代码如下

public class XXXService() {
   private XXXDao dao = new XXXDao();
   public void serviceMethod() {
       try {
          JdbcUtils.beginTransaction();//开启事务
          dao.daoMethod1();//操作1
          dao.daoMethod2(); //操作2
          JdbcUtils.commitTransaction();//提交事务
        } catch(Exception e) {
           JdbcUtils.rollbackTransaction();//回滚
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

要保证两个方法中获取的conn是同一个

DAO代码如下

public void daoMethod1() {
	Connection con = JdbcUtils.getConnection();
}

public void daoMethod2() {
	Connection con = JdbcUtils.getConnection();
}
1
2
3
4
5
6
7

在Service层方法中调用了JdbcUtil.beginTransaction()方法后,JdbcUtil要做准备好一个已经调用了setAuthCommitted(false)方法的Connection对象,因为在Service中调用JdbcUtils.beginTransaction()之后,马上就会调用DAO的方法,而在DAO方法中会调用JdbcUtils.getConnection()方法。这说明JdbcUtils要在getConnection()方法中返回刚刚准备好的,已经设置了手动提交的Connection对象。

在JdbcUtils中创建一个Connection con属性,当它为null时,说明没有事务!当它不为null时,表示开启了事务。

  • 在没有开启事务时,可以调用“开启事务”方法;
  • 在开启事务后,可以调用“提交事务”和“回滚事务”方法;
  • getConnection()方法会在con不为null时返回con,在con为null时,从连接池中返回连接。

beginTransaction()

  • 判断con是否为null,如果不为null,就抛出异常!
  • 如果con为null,那么从连接池中获取一个Connection对象,赋值给con!然后设置它为“手动提交”。

getConnection()

  • 判断con是否为null,如果为null说明没有事务,那么从连接池获取一个连接返回;
  • 如果不为null,说明已经开始了事务,那么返回con属性返回。这说明在con不为null时,无论调用多少次getConnection()方法,返回的都是同个Connection对象。

commitTransaction()

  • 判断con是否为null,如果为null,说明没有开启事务就提交事务,那么抛出异常;
  • 如果con不为null,那么调用con的commit()方法来提交事务;
  • 调用con.close()方法关闭连接;
  • con = null,这表示事务已经结束!

rollbackTransaction()

  • 判断con是否为null,如果为null,说明没有开启事务就回滚事务,那么抛出异常;
  • 如果con不为null,那么调用con的rollback()方法来回滚事务;
  • 调用con.close()方法关闭连接;
  • con = null,这表示事务已经结束!

# 2.4.1、JdbcUtils代码

package com.qfedu.utils;

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class JdbcUtils {
    private static DataSource dataSource;
    private static Connection connection;

    static {
        try {
            Properties prop = new Properties();
            InputStream in = JdbcUtils.class.getResourceAsStream("/jdbc.properties");
            prop.load(in);

            //创建DataSource
            dataSource = DruidDataSourceFactory.createDataSource(prop);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //获取连接
    public static Connection getConnection() throws SQLException {
        if(connection != null) {
            return connection;
        }
        return dataSource.getConnection();
    }

    //开启事务
    public static void beginTransaction() throws SQLException {
        if(connection != null) {
            throw new SQLException("事务已经开启,在没有结束当前事务时,不能再开启事务!");
        }

        connection = dataSource.getConnection();
        connection.setAutoCommit(false);
    }

    //提交事务
    public static void commitTransaction() throws SQLException {
        if(connection == null) {
            throw new SQLException("当前没有事务,所以不能提交事务!");
        }

        connection.commit();
        connection.close();
        connection = null;
    }

    //回滚事务
     public static void rollbackTransaction() throws SQLException {
        if(connection == null) {
            throw new SQLException("当前没有事务,所以不能回滚事务!");
        }

        connection.rollback();
        connection.close();
        connection = null;
     }

    //释放资源
    public static void close(Connection connection, Statement statement, ResultSet rSet) {
        try {
            if(rSet != null) {
                rSet.close();
            }

            if(statement != null) {
                statement.close();
            }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

# 2.4.2、DAO代码

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {  
    /*
     * 提款withdrawal
     * */
    public void withdrawal(String cardNum, double money) throws SQLException {
        String sql = "update account set money=money-? where cardNum=?";
        PreparedStatement pstmt = null;

        Connection conn = JdbcUtils.getConnection();
        //操作
        pstmt = conn.prepareStatement(sql);
        //设置参数
        pstmt.setDouble(1, money);
        pstmt.setString(2, cardNum);
        //发送SQL
        pstmt.executeUpdate();

        pstmt.close();
    }

    /*
     * 存款deposit
     * */
    public void deposit(String cardNum, double money) throws SQLException {
        String sql = "update account set money=money+? where cardNum=?";

        PreparedStatement pstmt = null;

        Connection conn = JdbcUtils.getConnection();
        //操作
        pstmt = conn.prepareStatement(sql);
        //设置参数
        pstmt.setDouble(1, money);
        pstmt.setString(2, cardNum);
        //发送SQL
        pstmt.executeUpdate();

        pstmt.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 2.4.3、Service代码

import com.qfedu.dao.AccountDao;
import com.qfedu.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.SQLException;

public class AccountService {
    private AccountDao accountDao = new AccountDao();
    //转账操作
    public void trans(String src, String dst, double money) {

        try {
            //开启事务
            JdbcUtils.beginTransaction();
            //取款
            accountDao.withdrawal(src, money);
			
            //用来测试回滚
            //int i = 100/0;
			//存款
            accountDao.deposit(dst, money);
            //提交事务
            JdbcUtils.commitTransaction();
        } catch (Exception e) { //注意这里异常的类型
            e.printStackTrace();
            try {
                //回滚
                JdbcUtils.rollbackTransaction();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 2.5、JdbcUtils完善(必须掌握)

ThreadLocal:本地线程对象,内部变量是线程私有的,专属于每一个线程的。

现在JdbcUtils有个问题,如果有两个线程!第一个线程调用了beginTransaction()方法,另一个线程再调用beginTransaction()方法时,因为con已经不再为null,所以就会抛出异常了。

我们希望JdbcUtils可以多线程环境下被使用!最好的方法是为每个线程提供一个Connection,这样每个线程都可以开启自己的事务了。

ThreadLocal类只有三个方法:

  • void set(T value):保存值;
  • T get():获取值;
  • void remove():移除值。

ThreadLocal内部其实是个Map来保存数据。虽然在使用ThreadLocal时只给出了值,没有给出键,其实它内部使用了当前线程做为键。

ThreadLocal使用

public class MyTestThreadLocal {
    private static ThreadLocal<String> tl = new ThreadLocal<>();

    /**
     * set() 在当前线程中存放数据
     * get() 获取当前线程的数据 - 无法获取其他线程的数据
     * remove() 删除当前线程的数据
     * @param args
     */
    public static void main(String[] args) {
        //Runnable
        Thread th1 = new Thread(() -> {
            tl.set("哈哈哈哈.....");

            for (int i = 0; i < 100; i++) {
                if(i == 5) {
                    tl.remove();
                }
                System.out.println(Thread.currentThread().getName() + ":" + tl.get());
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        th1.setName("线程1");

        Thread th2 = new Thread(() -> {
            tl.set("嘿嘿嘿嘿.....");

            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + tl.get());
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        th2.setName("线程2");

        //启动线程
        th1.start();
        th2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

使用ThreadLocal修改JdbcUtils

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class JdbcUtils {
    private static DataSource dataSource;
    private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    static {
        try {
            Properties prop = new Properties();
            InputStream in = JdbcUtils.class.getResourceAsStream("/jdbc.properties");
            prop.load(in);

            //创建DataSource
            dataSource = DruidDataSourceFactory.createDataSource(prop);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //获取连接
    public static Connection getConnection() throws SQLException {
        Connection connection = tl.get();

        if(connection != null) {
            return connection;
        }
        return dataSource.getConnection();
    }

    //开启事务
    public static void beginTransaction() throws SQLException {
        Connection connection = tl.get();
        if(connection != null) {
            throw new SQLException("事务已经开启,在没有结束当前事务时,不能再开启事务!");
        }

        connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        tl.set(connection);
    }

    //提交事务
    public static void commitTransaction() throws SQLException {
        Connection connection = tl.get();
        if(connection == null) {
            throw new SQLException("当前没有事务,所以不能提交事务!");
        }

        connection.commit();
        connection.close();
        tl.remove();
    }

    //回滚事务
     public static void rollbackTransaction() throws SQLException {
        Connection connection = tl.get();
        if(connection == null) {
            throw new SQLException("当前没有事务,所以不能回滚事务!");
        }

        connection.rollback();
        connection.close();
        tl.remove();
     }

    //释放资源
    public static void close(Connection connection, Statement statement, ResultSet rSet) {
        try {
            if(rSet != null) {
                rSet.close();
            }

            if(statement != null) {
                statement.close();
            }

            if(connection != null) {
                connection.close();
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
上次更新: 2024/4/13