通过上一章介绍,我们已经基本了解ThreadLocal的特点。但是它具体是运用在什么场景中呢? 接下来让我们看一个案例: 事务操作。
1 转账案例
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) 引入依赖
<dependency><groupId>com.mchange</groupId><artifactId>c3p0</artifactId><version>0.9.5.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.19</version></dependency><dependency><groupId>com.mchange</groupId><artifactId>mchange-commons-java</artifactId><version>0.2.12</version></dependency>
(4) C3P0配置文件和工具类
<c3p0-config><!-- 使用默认的配置读取连接池对象 --><default-config><!-- 连接参数 --><property name="driverClass">com.mysql.jdbc.Driver</property><property name="jdbcUrl">jdbc:mysql://192.168.222.156:3306/test_db</property><property name="user">root</property><property name="password">123456</property><!-- 连接池参数 --><property name="initialPoolSize">5</property><property name="maxPoolSize">10</property><property name="checkoutTimeout">3000</property></default-config></c3p0-config>
(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 转账 100String 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) 开启事务的注意点:
- 为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个: service层开启事务的connection需要跟dao层访问数据库的connection保持一致
- 线程并发情况下, 每个线程只能操作各自的 connection
2 常规解决方案
2.1 常规方案的实现
基于上面给出的前提, 大家通常想到的解决方案是 :
- 传参: 从service层将connection对象向dao层传递
- 加锁
以下是代码实现修改的部分:
(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 常规方案的弊端
上述方式我们看到的确按要求解决了问题,但是仔细观察,会发现这样实现的弊端:
- 直接从service层传递connection到dao层, 造成代码耦合度提高
- 加锁会造成线程失去并发性,程序性能降低
3 ThreadLocal解决方案
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方案有两个突出的优势:
- 传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题。
- 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。