在使用事务解决了该问题之后,在文章后面还有总结、注意点、引申出来的其他解决问题是否可行!全面的帮大家了解深入事务思想解决所在问题!点赞支持下呗!😁
考虑问题:
如果在转账业务中途,付款方账户已扣除了转账金额,而收款方因异常则收不到转账金额。在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等等,在以后的框架中,我们可以引用其他对象实现复用此项目,这个传入参数虽然解决了目前功能上的问题,但是脱离了我们的初衷(再次强调复用性!),这就会产生不能复用的问题!