ThreadLocal应用场景-事务案例

第一、ThreadLocal介绍
1.线程并发: 在多线程并发的场景下
2.传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3.线程隔离: 每个线程的变量都是独立的,不会相互影响
第二、常用方法
在这里插入图片描述
第三、事务案例
1、场景构建
这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。 案例的实现就简单的用mysql数据库,JDBC 和 C3P0 框架实现。
2、数据准备,新建表t_account,初始化数据如下
在这里插入图片描述
3、常规解决方案
(1)为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个:service层开启事务的connection需要跟dao层访问数据库的connection保持一致。
(2)线程并发情况下, 每个线程只能操作各自的connection。
a、新建maven工程,pom.xml代码如下

  <dependencies>
 	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<version>5.1.12</version>
	</dependency>
	<dependency>
	    <groupId>com.mchange</groupId>
	    <artifactId>c3p0</artifactId>
	    <version>0.9.5.2</version>
	</dependency>
  </dependencies>

b、src目录添加配置文件c3p0-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
 <!-- 使用默认的配置读取连接池对象 -->
 <default-config>
 	<!--  连接参数 -->
   <property name="driverClass">com.mysql.jdbc.Driver</property>
   <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
   <property name="user">root</property>
   <property name="password">1234</property>
   <!-- 连接池参数 -->
    <property name="initialPoolSize">5</property>
    <property name="maxPoolSize">10</property>
    <property name="checkoutTimeout">3000</property>
  </default-config>
</c3p0-config>

c、新建工具类com.util.JdbcUtils.java

package com.util;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
 * 工具类
 * @author shixiangcheng
 * 2020-07-18
 */
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();
        }
    }
}

d、新建com.dao.AccountDao.java。

package com.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.util.JdbcUtils;
/**
 * Dao层
 * @author shixiangcheng
 * 2020-07-18
 */
public class AccountDao {
	public void out(Connection conn, String outUser, int money) throws SQLException{
        String sql = "update t_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 t_account set money = money + ? where name = ?";
	    PreparedStatement pstm = conn.prepareStatement(sql);
	    pstm.setInt(1,money);
	    pstm.setString(2,inUser);
	    pstm.executeUpdate();
	    JdbcUtils.release(pstm);
	 }
}

e、新建com.service.AccountService.java。 一个转出,一个转入。这些操作是需要具备原子性的,不可分割。不然就有可能出现数据修改异常情况。

package com.service;
import java.sql.Connection;
import com.dao.AccountDao;
import com.util.JdbcUtils;
/**
 * Service层
 * @author shixiangcheng
 * 2020-07-18
 */
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;
        }
    }
}

f、新建com.web.AccountWeb.java

package com.web;
import com.service.AccountService;
/**
 * web层
 * @author shixiangcheng
 * 2020-07-18
 */
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("转账成功!");
        }
    }
}

g、运行web类

七月 18, 2020 6:01:46 下午 com.mchange.v2.log.MLog 
信息: MLog clients using java 1.4+ standard logging.
七月 18, 2020 6:01:47 下午 com.mchange.v2.c3p0.C3P0Registry 
信息: Initializing c3p0-0.9.5.2 [built 08-December-2015 22:06:04 -0800; debug? true; trace: 10]
七月 18, 2020 6:01:47 下午 com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource 
信息: Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 3000, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, dataSourceName -> 1hge15yabr0mkob168zbu4|1424e82, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, extensions -> {}, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceSynchronousCheckins -> false, forceUseNamedDriverClass -> false, identityToken -> 1hge15yabr0mkob168zbu4|1424e82, idleConnectionTestPeriod -> 0, initialPoolSize -> 5, jdbcUrl -> jdbc:mysql://localhost:3306/test, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 10, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 3, numHelperThreads -> 3, preferredTestQuery -> null, privilegeSpawnedThreads -> false, properties -> {user=******, password=******}, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, userOverrides -> {}, usesTraditionalReflectiveProxies -> false ]
java.lang.ArithmeticException: / by zero
	at com.service.AccountService.transfer(AccountService.java:23)
	at com.web.AccountWeb.main(AccountWeb.java:15)
转账失败!

h、观察数据
在这里插入图片描述
4、常规解决方案的弊端
a.直接从service层传递connection到dao层, 造成代码耦合度提高
b.加锁会造成线程失去并发性,程序性能降低
5、ThreadLocal解决方案
ThreadLocal方案的实现像这种需要在项目中进行数据传递线程隔离的场景,我们不妨用ThreadLocal来解决。
a、com.util.JdbcUtils.java修改如下

package com.util;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
 * 工具类
 * @author shixiangcheng
 * 2020-07-18
 */
public class JdbcUtils {
    //ThreadLocal对象 : 将connection绑定在当前线程中
    private static final ThreadLocal<Connection> tl = new ThreadLocal();
    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    // 获取连接
    public static Connection getConnection() throws SQLException {
        Connection conn = tl.get();取出当前线程绑定的connection对象
        if (conn == null) {//如果没有,则从连接池中取出
            conn = ds.getConnection();
            tl.set(conn);//再将connection对象绑定到当前线程中
        }
        return conn;
    }
    //释放资源
    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() {
        try {
            Connection conn = getConnection();
            conn.commit();//提交事务
            tl.remove();//解除绑定
            conn.close();//释放连接
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public static void rollbackAndClose() {
        try {
            Connection conn = getConnection();
            conn.rollback();//回滚事务
            tl.remove();//解除绑定
            conn.close();//释放连接
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

b、com.dao.AccountDao.java修改如下

package com.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.util.JdbcUtils;
/**
 * Dao层
 * @author shixiangcheng
 * 2020-07-18
 */
public class AccountDao {
	public void out(String outUser, int money) throws SQLException {
        String sql = "update t_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);
	}
    public void in(String inUser, int money) throws SQLException {
        String sql = "update t_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);
    }
}

c、com.service.AccountService.java修改如下

package com.service;
import java.sql.Connection;
import com.dao.AccountDao;
import com.util.JdbcUtils;
/**
 * Service层
 * @author shixiangcheng
 * 2020-07-18
 */
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;
    }
}

d、运行web类,效果和常规方案一致。
6、ThreadLocal方案的好处
a、传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题。
b、线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。

欢迎大家积极留言交流学习心得,点赞的人最美丽,谢谢

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值