首页 > 技术文章 > 简单易懂的ThreadLocal用于事务控制

guangwenyin 2021-12-04 18:02 原文

一  场景介绍

很多时候离开场景谈技术是不太适合的,也是不太容易理解的,毕竟天上飞的理念还要有落地的实现才有意义。这里我们以常见的转账为例。

数据库threadlocal有一张表account,里面有两个用户jianlinwang和guangwenyin,现在jianlinwang要给guangwenyin转一个亿。如下图

 

二  问题引出

这里我们用原生jdbc和mysql驱动来实现,涉及如下两个操作

jianlinwang转出:update account set balance = balance - ? where id = ? 

guangwenyin转入:update account set balance = balance + ? where id = ? 

显然,我们的目的是让这两个操作要么同时成功,要么同时失败,那么两个操作放在一个事务里面就行,接下来的问题是怎么才能把两个操作放在一个事务里面呢,又怎么控制事务的提交和回滚呢?

三  常规解决方案

我们采用普遍使用的三层架构,dao层操作数据库,service层调用dao层处理业务,web层调用service层获取业务处理结果。

在程序中实现我们想要的结果,很重要的两个问题:1 两个数据库操作用的是同一个connection   2  保证线程隔离 

项目结构如下:

 

 

 ----------------------------------------------------------

web

-------------------------------------------------------

package com.study.threadlocal.web;

import com.study.threadlocal.service.AccountService;
import java.sql.SQLException;
/**
* @Author guangwenyin
* @Date 2021/12/3
*/
public class Web {
public static void main(String[] args) throws SQLException {
AccountService service=new AccountService();
boolean result = service.transfer(1, 2, 1.0);
if (result){
System.out.println("转账成功");
}else {
System.out.println("转账失败");
}
}
}

 ------------------------------------------------------

AccountService

-------------------------------------------------------

package com.study.threadlocal.service;

import com.study.threadlocal.dao.AccountDao;
import com.study.threadlocal.util.JDBCUtils;
import java.sql.Connection;
import java.sql.SQLException;

/**
* @Author guangwenyin
* @Date 2021/12/3
*/
public class AccountService {

AccountDao dao=new AccountDao();

/**
* 转账的方法
* @param fromUserId 转出用户id
* @param toUserId 转入用户id
* @param money 转账金额
* @return 转账是否成功的标识 ,false:失败 ,true:成功
*/
public boolean transfer(Integer fromUserId,Integer toUserId,Double money) throws SQLException {

Connection connection=null;
try {
synchronized (AccountService.class){
//获取数据库链接
connection= JDBCUtils.getConnection();
//关闭自动提交,开启事务
connection.setAutoCommit(false);
dao.add(toUserId,money,connection);
//异常代码
// int i=1/0;
dao.minus(fromUserId,money,connection);
//成功提交
connection.commit();
connection.close();
return true;
}
}catch (Exception e){
e.printStackTrace();
assert connection != null;
//失败回滚
connection.rollback();
connection.close();
return false;
}

}
}

 ------------------------------------------------------

AccountDao

-------------------------------------------------------

package com.study.threadlocal.dao;

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

/**
* @Author guangwenyin
* @Date 2021/12/3
*/
public class AccountDao {

/**
* 增加用户金额的方法
* @param userId 转入用户的id
* @param money 转入金额
* @param connection 数据库链接
* @throws SQLException
*/
public void add (Integer userId, Double money,Connection connection) throws SQLException {
// Connection connection = JDBCUtils.getConnection();//这里每次调用方法获取的链接不是同一个
String sql="update account set balance = balance + ? where id = ?";
PreparedStatement preparedStatement=connection.prepareStatement(sql);
preparedStatement.setDouble(1,money);
preparedStatement.setInt(2,userId);
preparedStatement.executeUpdate();
// connection.close();//这里不能关闭链接

}

/**
* 扣减用户金额的方法
* @param userId 转出用户的id
* @param money 转出金额
* @param connection 数据库链接
* @throws SQLException
*/
public void minus (Integer userId, Double money,Connection connection) throws SQLException {
// Connection connection = JDBCUtils.getConnection();//这里每次调用方法获取的链接不是同一个,所以不能这样获取
String sql="update account set balance = balance - ? where id = ?";
PreparedStatement preparedStatement=connection.prepareStatement(sql);
preparedStatement.setDouble(1,money);
preparedStatement.setInt(2,userId);
preparedStatement.executeUpdate();
// connection.close();//这里不能关闭链接
}
}

 ------------------------------------------------------

JDBCUtils

-------------------------------------------------------

package com.study.threadlocal.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

/**
* @Author guangwenyin
* @Date 2021/12/3
*/
public class JDBCUtils {

/**
*
* @Description 获取数据库的连接
*/
public static Connection getConnection() throws Exception {
// 1.读取配置文件中的基本信息
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");

Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("username");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");

// 2.加载驱动
Class.forName(driverClass);

// 3.获取连接
Connection connection = DriverManager.getConnection(url, user, password);
return connection;
}

}

 ------------------------------------------------------

jdbc.properties

-------------------------------------------------------

username=root
password=123456
url=jdbc:mysql://localhost:3306/threadlocal?rewriteBatchedStatements=true
driverClass=com.mysql.jdbc.Driver

出现异常

 

 

 

 太可惜了!

 

正常情况

 

 

 

 

 太开心了,小目标实现了!

 

四  threadlocal解决方案

那么问题来了,常规解决方案能解决问题,为什么还要提出threadlocal解决方案呢?

原因主要有两个:一方面,上面的常规解决方案增加了代码的耦合度,为什么这么说,service层在调用dao层时,为了保证用的是同一个connection,使用了传参的方式,而connection是与业务无关的。另一方面,降低了系统性能,这个很好理解,因为加锁了。

threadlocal解决方案代码:

 ------------------------------------------------------

web

-------------------------------------------------------

package com.study.threadlocal.web;
import com.study.threadlocal.service.AccountService;
import java.sql.SQLException;

/**
* @Author guangwenyin
* @Date 2021/12/3
*/
public class Web {
public static void main(String[] args) throws SQLException {
AccountService service=new AccountService();
boolean result = service.transfer(1, 2, 1.0);
if (result){
System.out.println("转账成功");
}else {
System.out.println("转账失败");
}
}
}


 ------------------------------------------------------

AccountService

-------------------------------------------------------

package com.study.threadlocal.service;

import com.study.threadlocal.dao.AccountDao;
import com.study.threadlocal.util.JDBCUtils;
import java.sql.Connection;
import java.sql.SQLException;

/**
* @Author guangwenyin
* @Date 2021/12/3
*/
public class AccountService {

AccountDao dao=new AccountDao();

/**
*
* @Description 转账的方法
*/
public boolean transfer(Integer fromUserId,Integer toUserId,Double money) throws SQLException {

Connection connection=null;
try {
//不再需要同步处理
connection= JDBCUtils.getConnection();
connection.setAutoCommit(false);
System.out.println("AccountService---connection---"+connection);
dao.add(toUserId,money);
int i=1/0;//异常代码
dao.minus(fromUserId,money);
connection.commit();
connection.close();
return true;
}catch (Exception e){
e.printStackTrace();
assert connection != null;
connection.rollback();
connection.close();
return false;
}

}
}

 ------------------------------------------------------

AccountDao

-------------------------------------------------------

package com.study.threadlocal.dao;

import com.study.threadlocal.util.JDBCUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;

/**
* @Author guangwenyin
* @Date 2021/12/3
*/
public class AccountDao {

/**
* 增加用户金额的方法
* @param userId 转入用户的id
* @param money 转入金额
* @throws Exception
*/
public void add (Integer userId, Double money) throws Exception {
Connection connection = JDBCUtils.getConnection();//同一个线程通过此方法获取的是同一个链接
System.out.println("AccountDao-------connection---"+connection);

String sql="update account set balance = balance + ? where id = ?";
PreparedStatement preparedStatement=connection.prepareStatement(sql);
preparedStatement.setDouble(1,money);
preparedStatement.setInt(2,userId);
preparedStatement.executeUpdate();
// JDBCUtils.closeResource(connection,preparedStatement);//链接不能关闭

}

/**
* 扣减用户金额的方法
* @param userId
* @param money
* @throws Exception
*/
public void minus (Integer userId, Double money) throws Exception {
Connection connection = JDBCUtils.getConnection();//同一个线程通过此方法获取的是同一个链接
String sql="update account set balance = balance - ? where id = ?";
PreparedStatement preparedStatement=connection.prepareStatement(sql);
preparedStatement.setDouble(1,money);
preparedStatement.setInt(2,userId);
preparedStatement.executeUpdate();
// JDBCUtils.closeResource(connection,preparedStatement);//链接不能关闭
}
}

 ------------------------------------------------------

JDBCUtils

-------------------------------------------------------

package com.study.threadlocal.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;

/**
* @Author guangwenyin
* @Date 2021/12/3
*/
public class JDBCUtils {


static ThreadLocal<Connection> threadLocal=new ThreadLocal<>();

/**
*
* @Description 获取数据库的连接
*/
public static Connection getConnection() throws Exception {
// 0,先从threadlocal里面获取链接,有则返回
Connection connection = threadLocal.get();
if(!(connection==null)){
return connection;
}

// 1.读取配置文件中的基本信息
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("username");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");

// 2.加载驱动
Class.forName(driverClass);

// 3.获取连接,并放入threadlocal
connection = DriverManager.getConnection(url, user, password);
threadLocal.set(connection);
return connection;
}
}


 ------------------------------------------------------

jdbc.properties

-------------------------------------------------------

username=root
password=123456
url=jdbc:mysql://localhost:3306/threadlocal?rewriteBatchedStatements=true
driverClass=com.mysql.jdbc.Driver

出现异常

 

 

 

 正常情况

 

 

 

 nice!

从以上案例可以看出通过threadlocal就避免了参数传递带来的耦合,同时也避免了同步方式带来的性能损失。


学无止境,让学习成为一种习惯。

本人水平有限,有不对的地方请指教,谢谢。

推荐阅读