ThreadLocal(2):运用场景

通过上一章介绍,我们已经基本了解ThreadLocal的特点。但是它具体是运用在什么场景中呢? 接下来让我们看一个案例: 事务操作。

1 转账案例

1.1 场景构建

​ 这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。

​ 案例的实现主要用mysql数据库,JDBC 和 C3P0 框架。以下是详细代码 :

​ (1) 项目结构

ThreadLocal(2):运用场景_第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);

ThreadLocal(2):运用场景_第2张图片

(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("转账成功!");
        }
    }
}

启动以后效果为

ThreadLocal(2):运用场景_第3张图片

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方案有两个突出的优势:

  • 传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题。
  • 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。

你可能感兴趣的:(java,java)