通过上一章介绍,我们已经基本了解ThreadLocal的特点。但是它具体是运用在什么场景中呢? 接下来让我们看一个案例: 事务操作。
1.1 场景构建
这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。
案例的实现主要用mysql数据库,JDBC 和 C3P0 框架。以下是详细代码 :
(1) 项目结构
(2) 数据准备
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
-- 初始化数据
insert into account values(null, 'Jack', 1000);
insert into account values(null, 'Rose', 0);
(3) 引入依赖
com.mchange
c3p0
0.9.5.2
mysql
mysql-connector-java
8.0.19
com.mchange
mchange-commons-java
0.2.12
(4) C3P0配置文件和工具类
com.mysql.jdbc.Driver
jdbc:mysql://192.168.222.156:3306/test_db
root
123456
5
10
3000
(5)工具类 : JdbcUtils
package com.example.demo.utils;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class JdbcUtils {
// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
// 获取连接
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
//释放资源
public static void release(AutoCloseable... ios){
for (AutoCloseable io : ios) {
if(io != null){
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void commitAndClose(Connection conn) {
try {
if(conn != null){
//提交事务
conn.commit();
//释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose(Connection conn) {
try {
if(conn != null){
//回滚事务
conn.rollback();
//释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
(6) dao层代码 : AccountDao
package com.example.demo.dao;
import com.example.demo.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AccountDao {
public void out(String outUser, int money) throws SQLException {
String sql = "update account set money = money - ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
JdbcUtils.release(pstm,conn);
}
public void in(String inUser, int money) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
JdbcUtils.release(pstm,conn);
}
}
(7)service层代码 : AccountService
package com.example.demo.service;
import com.example.demo.dao.AccountDao;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
// 转出
ad.out(outUser, money);
// 转入
ad.in(inUser, money);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
(8)web层代码 : AccountWeb
package com.example.demo.web;
import com.example.demo.service.AccountService;
public class AccountWeb {
public static void main(String[] args) {
// 模拟数据 : Jack 给 Rose 转账 100
String outUser = "Jack";
String inUser = "Rose";
int money = 100;
AccountService as = new AccountService();
boolean result = as.transfer(outUser, inUser, money);
if (result == false) {
System.out.println("转账失败!");
} else {
System.out.println("转账成功!");
}
}
}
启动以后效果为
1.2 引入事务
案例中的转账涉及两个DML操作: 一个转出,一个转入。这些操作是需要具备原子性的,不可分割。不然就有可能出现数据修改异常情况。
package com.example.demo.service;
import com.example.demo.dao.AccountDao;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
// 转出
ad.out(outUser, money);
// 模拟转账过程中的异常
int i = 1/0;
// 转入
ad.in(inUser, money);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
运行后效果如下:
没有事务的情况下,操作缺乏原子性。
所以这里就需要操作事务,来保证转出和转入操作具备原子性,要么同时成功,要么同时失败。
(1)JDBC中关于事务的操作的api
Connection接口的方法 | 作用 |
void setAutoCommit(false) | 禁用事务自动提交(改为手动) |
void commit(); | 提交事务 |
void rollback(); | 回滚事务 |
(2) 开启事务的注意点:
2.1 常规方案的实现
基于上面给出的前提, 大家通常想到的解决方案是 :
以下是代码实现修改的部分:
(1 ) AccountService类
package com.example.demo.service;
import com.example.demo.dao.AccountDao;
import com.example.demo.utils.JdbcUtils;
import java.sql.Connection;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
//线程并发情况下,为了保证每个线程使用各自的connection,故加锁
synchronized (AccountService.class) {
Connection conn = null;
try {
conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
// 转出
ad.out(conn, outUser, money);
// 模拟转账过程中的异常
// int i = 1/0;
// 转入
ad.in(conn, inUser, money);
//事务提交
JdbcUtils.commitAndClose(conn);
} catch (Exception e) {
e.printStackTrace();
//事务回滚
JdbcUtils.rollbackAndClose(conn);
return false;
}
return true;
}
}
}
(2) AccountDao类(这里需要注意的是:connection不能在dao层释放,要在service层,不然在dao层释放,service层就无法使用了)
package com.example.demo.dao;
import com.example.demo.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AccountDao {
public void out(Connection conn, String outUser, int money) throws SQLException {
String sql = "update account set money = money - ? where name = ?";
//注释从连接池获取连接的代码,使用从service中传递过来的connection
// Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1, money);
pstm.setString(2, outUser);
pstm.executeUpdate();
//连接不能在这里释放,service层中还需要使用
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
public void in(Connection conn, String inUser, int money) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
// Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1, money);
pstm.setString(2, inUser);
pstm.executeUpdate();
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
}
2.2 常规方案的弊端
上述方式我们看到的确按要求解决了问题,但是仔细观察,会发现这样实现的弊端:
3.1 ThreadLocal方案的实现
像这种需要在项目中进行数据传递和线程隔离的场景,我们不妨用ThreadLocal来解决:
(1)工具类的修改:加入ThreadLocal
package com.example.demo.service;
import com.example.demo.dao.AccountDao;
import com.example.demo.utils.JdbcUtils;
import java.sql.Connection;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
Connection conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
// 转出 : 这里不需要传参了 !
ad.out(outUser, money);
// 模拟转账过程中的异常
// int i = 1 / 0;
// 转入
ad.in(inUser, money);
//事务提交
JdbcUtils.commitAndClose();
} catch (Exception e) {
e.printStackTrace();
//事务回滚
JdbcUtils.rollbackAndClose();
return false;
}
return true;
}
}
(2) AccountService类的修改:不需要传递connection对象
package com.example.demo.dao;
import com.example.demo.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AccountDao {
public void out(String outUser, int money) throws SQLException {
String sql = "update account set money = money - ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
//照常使用
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
public void in(String inUser, int money) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
}
(3)AccountDao类的修改:照常使用
package com.example.demo.dao;
import com.example.demo.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AccountDao {
public void out(String outUser, int money) throws SQLException {
String sql = "update account set money = money - ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
//照常使用
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
public void in(String inUser, int money) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
}
3.2 ThreadLocal方案的好处
从上述的案例中我们可以看到, 在一些特定场景下,ThreadLocal方案有两个突出的优势: