初识事务

事务(transaction)的基本认识


事务的简介

什么是事务?事务其实就是将一件事情中的n个组成单元组合到一起,这n个组成单元要么一起成功,要么一起失败。


事务的特性

事务有四个特性:原子性、隔离性、持久性、一致性。

1、原子性:事务是个不可分割的单位,事务中的操作要么都成功,要么都失败
2、一致性:在一个事务中,事务前后数据的完整性必须保持一致
3、隔离性:多个事务之间互不干扰,相互隔离
4、持久性:事务一旦提交,对数据库中数据的改变是永久性的


mysql中的事务

默认的事务:一条SQL语句就是一个事务,默认情况下开启并提交事务

手动事务:
1、显式开启事务start transaction (connnection.setAutoCommit(false));
2、事务提交(commit),从开启事务到提交事务之间所有的SQL语句都是有效的
3、事务的回滚(rollback):从开始事务到回滚事务之间所有的SQL语句都是无效的


使用原生的JDBC进行事务操作

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
package com.my.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JDBCDemo {

public static void main(String[] agrs) {
Connection conn = null ;
try {
// 注册数据库驱动(通过反射获取数据库驱动类)
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取数据库连接
conn = DriverManager.getConnection("jdbc:mysql:///mytomcat09?useSSL=false&serverTimezone=GMT%2B8", "mackvord", "12345678") ;
// 获取执行SQL语句的对象(平台)
Statement statement = conn.createStatement();
// 手动开启事务,设置默认提交false。在mysql中,默认情况下每一条SQL语句都是一个事务,并且默认执行
conn.setAutoCommit(false);
// 执行SQL操作
statement.executeUpdate("insert into account values(null,'tom',23000)");
statement.executeUpdate("insert into account values(null,'jack',15000)");
// 提交事务
conn.commit();
// 关闭资源
statement.close();
conn.close();
} catch (Exception e) {
try {
// 回滚事务
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
}
}

使用DBUtilis进行事务处理

以银行转账为例,一般银行转账需要转入账户的名称、转出的账户名称以及转账的金额,下面我将通过一个简单的例子模拟转账的场景,首先,我创建了一个jsp文件,里面写了一个简单的表单:

表单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!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>Insert title here</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/tranfer" method="post">
转入账户:<input type="text" name="in"><br />
转出账户:<input type="text" name="out"><br />
转账金额:<input type="text" name="money"><br />
<input type="submit" value="确认转账"/>
</form>
</body>
</html>

然后新建一个Servlet

Web层
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
package com.my.tranfer.web;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.my.tranfer.service.TranferService;

public class TranferServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

public TranferServlet() {
super();
}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取三个参数:转入账户、转出账户、转账金额
String in = request.getParameter("in");
String out = request.getParameter("out");
String moneyStr = request.getParameter("money");
double money = Double.parseDouble(moneyStr) ;

// 调用业务层的方法进行转账操作
TranferService service = new TranferService() ;
boolean isTranferSuccess = service.tranfer(in, out, money);

// 设置UTF-8编码
response.setContentType("text/html;charset=UTF-8");
if (isTranferSuccess) {
response.getWriter().write("转账成功!");
} else {
response.getWriter().write("转账失败!");
}
}

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

新建TranferService类处理业务逻辑

Service层
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
package com.my.tranfer.service;

import java.sql.SQLException;
import com.my.tranfer.dao.TranferDao;
import com.my.utils.MyDataSourceUtils;

public class TranferService {

/**
* 业务层处理转账的方法
* @param in 转入的账户
* @param out 转出的账户
* @param money 转账金额
* @return isTranferSuccess记录转账是否成功
*/
public boolean tranfer(String in, String out, double money) {
// 创建DAO层对象
TranferDao dao = new TranferDao() ;
// 定义一个布尔类型的变量,记录转账是否成功
boolean isTranferSuccess = true ;
Connection conn = null ;
try {
// 获取连接
conn = DataSourceUtils.getConnection() ;
// 开启事务
conn.setAutoCommit(false);
// 转出钱的方法
dao.tranferOut(conn, out, money) ;

// 模拟异常
//int i = 1/0 ;

// 转入钱的方法
dao.tranferIn(conn, in, money) ;
} catch (Exception e) {
// 出现异常后将isTranferSuccess设置为false
isTranferSuccess = false;
try {
// 回滚事务
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
} finally {
try {
// 提交事务
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
return isTranferSuccess ;
}

}

创建TranferDao类

DAO层
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
package com.my.tranfer.dao;

import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import com.my.utils.MyDataSourceUtils;

public class TranferDao {

/**
* 转入金额的方法
* @param in 转入的账户
* @param money 转入的金额
* @throws SQLException
*/
public void tranferIn(Connection conn, String in, double money) throws SQLException {
QueryRunner runner = new QueryRunner() ;
String sql = "update account set money = money+? where name = ?" ;
runner.update(conn, sql, money, in) ;
}

/**
* 转出金额的方法
* @param out 转出的账户
* @param money 转出的金额
* @throws SQLException
*/
public void tranferOut(Connection conn, String out, double money) throws SQLException {
QueryRunner runner = new QueryRunner() ;
String sql = "update account set money = money-? where name = ?" ;
runner.update(conn, sql, money, out) ;
}

}

其中用到的工具类以及xml文件代码如下:

DataSourceutils工具类
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
package com.my.utils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import com.mchange.v2.c3p0.ComboPooledDataSource;

public class DataSourceUtils {

private static DataSource dataSource = new ComboPooledDataSource();

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

// 直接可以获取一个连接池
public static DataSource getDataSource() {
return dataSource;
}

// 获取连接对象
public static Connection getConnection() throws SQLException {
return dataSource.getConnection() ;
}

// 获取当前的连接对象
public static Connection getCurrentConnection() throws SQLException {
Connection con = tl.get();
if (con == null) {
con = dataSource.getConnection();
tl.set(con);
}
return con;
}

// 开启事务
public static void startTransaction() throws SQLException {
Connection con = getCurrentConnection();
if (con != null) {
con.setAutoCommit(false);
}
}

// 事务回滚
public static void rollback() throws SQLException {
Connection con = getCurrentConnection();
if (con != null) {
con.rollback();
}
}

// 提交并且 关闭资源及从ThreadLocall中释放
public static void commitAndRelease() throws SQLException {
Connection con = getCurrentConnection();
if (con != null) {
// 事务提交
con.commit();
// 关闭资源
con.close();
// 从线程绑定中移除
tl.remove();
}
}

// 关闭资源方法
public static void closeConnection() throws SQLException {
Connection con = getCurrentConnection();
if (con != null) {
con.close();
}
}

public static void closeStatement(Statement st) throws SQLException {
if (st != null) {
st.close();
}
}

public static void closeResultSet(ResultSet rs) throws SQLException {
if (rs != null) {
rs.close();
}
}
}
c3p0.xml
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>

<default-config>
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql:///mytomcat09?useSSL=false&amp;serverTimezone=GMT%2B8</property>
<property name="user">mackvord</property>
<property name="password">12345678</property>
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">20</property>
</default-config>
</c3p0-config>

可以看到,上面的代码关于事务的处理是在service层中,但是有一点不好的地方是在Servlet层中创建了Connection对象,由于事务的处理需要先取得对应的Connection对象,所以为了保证获取到正确的连接对象并且避免在service层中创建Connection对象,可以新建一个工具类,使用TreadLocal绑定连接对象。

改进后的工具类
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
package com.my.utils;

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

import com.mchange.v2.c3p0.ComboPooledDataSource;

/**
* 封装事务提交、回滚、获取连接、绑定线程资源的操作
* @author LQZ
* @date 2018年7月15日
* @version 1.0
*/
public class MyDataSourceUtils {

/**
* 创建dataSource对象
*/
private static ComboPooledDataSource dataSource = new ComboPooledDataSource() ;

/**
* 创建本地线程绑定连接(Map集合)
*/
private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>() ;

/**
* 创建新的Connection对象
* @return Connection对象
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}

/**
* 获取线程池中绑定的连接
* @return
* @throws SQLException
*/
public static Connection getCurrentConnection() throws SQLException {
// 获取当前线程绑定的连接
Connection conn = tl.get();
if (conn == null) {
// 如果当前线程没有绑定的连接,则创建一个新的连接
conn = getConnection();
// 将新创建的连接绑定到ThreadLocal中
tl.set(conn);
}
return conn ;
}

/**
* 开启事务的方法
* @throws SQLException
*/
public static void startTransaction() throws SQLException {
Connection conn = MyDataSourceUtils.getCurrentConnection();
conn.setAutoCommit(false);
}

/**
* 回滚事务的方法
* @throws SQLException
*/
public static void rollback() throws SQLException {
MyDataSourceUtils.getCurrentConnection().rollback();
}

/**
* 提交事务的方法
* @throws SQLException
*/
public static void commit() throws SQLException {
Connection conn = getCurrentConnection();
conn.commit();
tl.remove();
conn.close();
}
}

重新修改service层的代码:

修改后的TranferService类
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
package com.my.tranfer.service;

import java.sql.SQLException;
import com.my.tranfer.dao.TranferDao;
import com.my.utils.MyDataSourceUtils;

/**
* 业务层方法,进行业务逻辑的处理
* @author LQZ
* @date 2018年7月15日
* @version 1.0
*/
public class TranferService {

/**
* 业务层处理转账的方法
* @param in 转入的账户
* @param out 转出的账户
* @param money 转账金额
* @return isTranferSuccess记录转账是否成功
*/
public boolean tranfer(String in, String out, double money) {
// 创建DAO层对象
TranferDao dao = new TranferDao() ;
// 定义一个布尔类型的变量,记录转账是否成功
boolean isTranferSuccess = true ;
try {
// 开启事务
+ MyDataSourceUtils.startTransaction();
// 转出钱的方法
+ dao.tranferOut(out, money) ;

// 转入钱的方法
+ dao.tranferIn(in, money) ;
} catch (Exception e) {
// 出现异常后将isTranferSuccess设置为false
isTranferSuccess = false;
try {
// 回滚事务
+ MyDataSourceUtils.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
} finally {
try {
// 提交事务
+ MyDataSourceUtils.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
return isTranferSuccess ;
}

}

修改DAO层的代码:

修改后的TranferDao类
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
package com.my.tranfer.dao;

import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import com.my.utils.MyDataSourceUtils;

/**
* DAO层的方法,进行数据库操作
* @author LQZ
* @date 2018年7月15日
* @version 1.0
*/
public class TranferDao {

/**
* 转入金额的方法
* @param in 转入的账户
* @param money 转入的金额
* @throws SQLException
*/
public void tranferIn(String in,double money) throws SQLException {
QueryRunner runner = new QueryRunner() ;
+ Connection conn = MyDataSourceUtils.getCurrentConnection();
String sql = "update account set money = money+? where name = ?" ;
+ runner.update(conn, sql, money, in) ;
}

/**
* 转出金额的方法
* @param out 转出的账户
* @param money 转出的金额
* @throws SQLException
*/
public void tranferOut(String out, double money) throws SQLException {
QueryRunner runner = new QueryRunner() ;
+ Connection conn = MyDataSourceUtils.getCurrentConnection();
+ String sql = "update account set money = money-? where name = ?" ;
runner.update(conn, sql, money, out) ;
}

}

在进行转账操作的时候会涉及到减去金额(转出账户),增加金额(转入账户),而事务的目的就是要确保转账的整个过程要么成功要么失败,避免出现转出账户金额减少了但转入账户金额却没有增加等一些异常情况的出现。


隔离性在并发访问下引发的问题

  • 脏读:A读取到B尚未提交的数据
  • 不可重复读:在同一个事务中,两次读取的数据内容不一致
  • 虚读/幻读:在一个事务中,两次读取的数据条数不一致

事务的隔离级别

  1. read uncommitted:读取尚未提交的数据—-无法解决脏读、不可重复读、虚读/幻读等问题
  2. read committed:读取已经提交的数据—-Oracle默认的隔离级别,可以解决脏读问题
  3. repeatable read:重复读取—-mysql默认的隔离级别,可以解决脏读以及不可重复读的问题
  4. serializable:串行化,安全级别最高—可以解决脏读、不可重复读、虚读/幻读问题

事务的隔离级别并不是越高越好,事务的安全级别越高其性能就越低,查看mysql中的事务隔离安全级别的命令:select @@tx_isolation,设置事务隔离安全级别的命令:set session transaction isolation level 安全级别


如果您觉得我的文章对您有帮助,请随意赞赏,您的支持将鼓励我继续创作!
0%