使用JDBC模拟银行分层处理转账出现的事务问题详解(转账中断,事务处理)

在使用事务解决了该问题之后,在文章后面还有总结、注意点、引申出来的其他解决问题是否可行!全面的帮大家了解深入事务思想解决所在问题!点赞支持下呗!😁


考虑问题:

如果在转账业务中途,付款方账户已扣除了转账金额,而收款方因异常则收不到转账金额。在SQL中该问题使用事务解决,则在JDBC中也是用事务解决此问题!


分层如下:

在这里插入图片描述


创建表的SQL命令如下:

create table t_account
(
    card_id  char(20)      not null
        primary key,
    password char(50)      not null,
    username char(20)      not null,
    balance  double(10, 2) not null,
    phone    char(11)      not null
) charset = utf8;

代码如下:

数据库连接工具类(DBUtils)

    import java.io.IOException;
    import java.io.InputStream;
    import java.sql.*;
    import java.util.Properties;
    
    public class DBUtils {
        private static final Properties PROPERTIES = new Properties();
    
        //所有操作即为单线程操作,应用了多个Connection对象,我们将一个线程绑定一个Connection连接对象使用
        private static final ThreadLocal<Connection> THREAD_LOCAL = new ThreadLocal<>();
    
        static {
            InputStream inputStream = DBUtils.class.getResourceAsStream("/db.properties");
            try {
                PROPERTIES.load(inputStream);
                Class.forName(PROPERTIES.getProperty("driver"));
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 获取连接对象
         */
        public static Connection getConnection() {
            //在ThreadLocal里取
            Connection connection = THREAD_LOCAL.get();
            //connection对象为空则创建连接对象
            if (connection == null) {
                try {
                    connection = DriverManager.getConnection(PROPERTIES.getProperty("url"), PROPERTIES.getProperty("username"), PROPERTIES.getProperty("password"));
                    THREAD_LOCAL.set(connection);
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            return connection;
        }
    
        /**
         * 释放资源
         */
        public static void closeAll(Connection connection, Statement statement, ResultSet resultSet) {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (statement != null) {
                    statement.close();
                }
                if (connection != null) {
                    connection.close();
                    /**
                     * 关闭连接后,移除线程中绑定的连接对象
                     */
                    THREAD_LOCAL.remove();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

账户实体类(Account)

    public class Account {
        private String card_id;
        private String password;
        private String username;
        private double balance;
        private String phone;
    
        public Account() {
        }
    
        public Account(String card_id, String password, String username, double balance, String phone) {
            this.card_id = card_id;
            this.password = password;
            this.username = username;
            this.balance = balance;
            this.phone = phone;
        }
    
        public String getCard_id() {
            return card_id;
        }
    
        public void setCard_id(String card_id) {
            this.card_id = card_id;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public double getBalance() {
            return balance;
        }
    
        public void setBalance(double balance) {
            this.balance = balance;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    
        @Override
        public String toString() {
            return "Account{" +
                    "card_id='" + card_id + '\'' +
                    ", password='" + password + '\'' +
                    ", username='" + username + '\'' +
                    ", balance=" + balance +
                    ", phone='" + phone + '\'' +
                    '}';
        }
    }

数据库操作持久层(AccountDaoImpl)

    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    public class AccountDaoImpl {
        private Connection connection = null;
        private PreparedStatement preparedStatement = null;
        private ResultSet resultSet = null;
    
        public int update(Account account) {
            connection = DBUtils.getConnection();
            String sql = "update t_account set password = ?, username = ?, balance = ?, phone = ? where card_id = ?";
            try {
                preparedStatement = connection.prepareStatement(sql);
                preparedStatement.setString(1, account.getPassword());
                preparedStatement.setString(2, account.getUsername());
                preparedStatement.setDouble(3, account.getBalance());
                preparedStatement.setString(4, account.getPhone());
                preparedStatement.setString(5, account.getCard_id());
                return preparedStatement.executeUpdate();
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                DBUtils.closeAll(null, preparedStatement, resultSet);
            }
            return 0;
        }
    
        public Account select(String card_id) {
            connection = DBUtils.getConnection();
            String sql = "select card_id, password, username, balance, phone from t_account where card_id = ?";
            Account account = null;
            try {
                preparedStatement = connection.prepareStatement(sql);
                preparedStatement.setString(1, card_id);
                resultSet = preparedStatement.executeQuery();
                if (resultSet.next()) {
                    account = new Account(resultSet.getString(1), resultSet.getString(2), resultSet.getString(3), resultSet.getDouble(4), resultSet.getString(5));
                }
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                DBUtils.closeAll(null, preparedStatement, resultSet);
            }
            return account;
        }
    }

Service业务层(AccountServiceImpl)

    import java.sql.Connection;
    import java.sql.SQLException;
    
    public class AccountServiceImpl {
        public String transfer(String username, String password, String toCard, double money) {//收参
            String result = "转账失败!";
    
            //组织业务功能
            AccountDaoImpl accountDao = new AccountDaoImpl();
            //拿一个连接对象
            Connection connection = null;
            //建立一个数据库连接
            connection = DBUtils.getConnection();
    
            try {
                //开启事务,并且关闭事务的自动提交
                connection.setAutoCommit(false);
    
                //2.1验证用户名是否存在
                Account account = accountDao.select(username);
                if (account == null) {
                    throw new RuntimeException("您输入的卡号不存在!");
                }
    
                //2.2验证密码是否正确
                if (!account.getPassword().equals(password)) {
                    throw new RuntimeException("密码错误!");
                }
    
                //2.3验证余额是否充足
                if (account.getBalance() < money) {
                    throw new RuntimeException("卡内余额不足!");
                }
    
                //2.4验证收款账户是否存在
                Account toAccount = accountDao.select(toCard);
                if (toAccount == null) {
                    throw new RuntimeException("收款卡号不存在!");
                }
    
                //2.5扣除付款卡号内的转账金额
                account.setBalance(account.getBalance() - money);
                accountDao.update(account);
    
                /**
                 * 模拟出现异常,导致程序终止
                 */
    //            int i = 10 / 0;
    
                //2.6增加收款卡号内的转账金额
                toAccount.setBalance(toAccount.getBalance() + money);
                accountDao.update(toAccount);
    
                //响应客户端
                result = "转账成功!";
    
                //执行到这里,没有异常,则正常提交事务!
                connection.commit();
            } catch (SQLException e) {
                e.printStackTrace();
                try {
                    //中途出现异常,回滚事务
                    connection.rollback();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            } finally {
                //关闭连接对象
                DBUtils.closeAll(connection, null, null);
            }
            return result;
        }
    }

转账测试类(TestTransfer)

    public class TestTransfer {
        public static void main(String[] args) {
            AccountServiceImpl accountService = new AccountServiceImpl();
    		//模拟客户端录入信息进行转账
            String result = accountService.transfer("1", "123456", "2", 5000);
            System.out.println(result);
        }
    }

测试结果如下:

初始化账户为:
在这里插入图片描述

转账后账户为:
在这里插入图片描述


注意: 在Service层注释了一个算数异常int i = 10 / 0; 用来模拟整个事务(整个转账操作看作一个事务)中途异常而终止查看是否用事务解决了该问题!


总结:

这条是总结,但也是需要注意的点(坑)。由于转账问题,需要介入事务解决!因为我们加入事务解决此问题。但是上文在DBUtiils数据库连接工具中,加入了一个局部变量(ThreadLocal并不是一个Thread,而是Thread的局部变量 )来绑定该线程中使用的Connection对象,使得在单线程中原使用的所有不同的Connection对象固定为了一个(即一个线程分配且固定一个私人对象)。这样保证了在DAO层和Service层使用都是同一个连接对象,而加入事务后就成功的解决了因异常而中断以至产生的种种问题。


注意点:

总结中说到,用ThreadLocal局部变量固定了连接对象来保证同一连接对象的使用。这里解释一下是因为connection.setAutoCommit(false);开启事务、connection.commit();提交事务、connection.rollback();回滚事务都需要上下层是同一个连接对象才可以解决此问题!如果我们没有固定此单线程的连接对象,则解决不了该问题!


引申出的问题:

这就引申出了小伙伴们的猜想,怎么解决呢?是不是单例模式可以创建对象解决此问题呢?假如在方法的参数列表中写入需要传入一个Connection对象可不可以解决呢?那我就在下面说一下这两个问题,小伙伴们看好了!

单例模式: 会出现一个问题,限制了创建对象,致使当前项目只能有一个客户端能连接使用转账功能(我们的产品就是为客户提供的,不能这么限制吧,如果只有一个人可以使用这怎么办呢?那么这个产品不就是废品了吗?对吧。继续看下一个吧!)

参数传递Connection: 如果将Service获得的Connection对象,传递给DAO各个方法。可以。但是定义接口是为了更容易更换实现!(强调复用性)而将Connection参数定义在接口方法中,就会污染当前接口,而无法复用。我们要知道JDBC是使用的Connection,而MyBatis使用SqlSession等等,在以后的框架中,我们可以引用其他对象实现复用此项目,这个传入参数虽然解决了目前功能上的问题,但是脱离了我们的初衷(再次强调复用性!),这就会产生不能复用的问题!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值