问题:
转账案例中,service层只是简单的实现了转账过程中变更数据库数据的操作,并没有进行相关的事务控制。如果在两个更新账户信息方法中间添加一个异常,第一个更新方法正常执行,而第二个方法则不会执行,造成了数据的不一致性。所以我们需要对业务处理过程添加事务控制,保证其满足事务的ACID原则。
accountDao.updateAcctByCardId(fromAcctInfo);
int i = 1 / 0;
accountDao.updateAcctByCardId(toAcctInfo);
问题分析:
(1)在dao层,是存在事务控制的,也就是JDBC中的事务控制,归根结底也就是Connection的事务,Connection.commit()提交事务,Connection.rollback()回滚事务。不过该事务默认是自动提交的。我们可以通过connection.setAutoCommit(false)来关闭JDBC的自动提交事务控制。
(2)在service层,调用了两次updateAcctByCardId方法,也就是创建了两个不同的connection对象,这样肯定就不属于同一个事务控制了。
解决思路:
(1)两次updateAcctByCardId方法必须使用同一个connection对象。在tomcat项目中,每一次请求就是一个单独的线程,贯穿始终,我们可以将connection对象与线程进行绑定,什么时候需要,就在当前线程获取就可以保证connection对象唯一了。所以,我们可以用ThreadLocal来进行解决
(2)将事务控制添加到service层,对业务处理的过程添加事务控制。
代码修改:
(1)采用ThreadLocal将connection对象与当前线程绑定
新建一个ConnectionUtils来管理connection对象,进而获取当前线程所绑定的连接。注意的是,该工具类必须要保证单例,如果直接采用new的方式来获取工具类对象,在创建多个ConnectionUtils 对象的同时,也创建了多个连接。
public class ConnectionUtils {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
//构造方法私有化,防止创建ConnectionUtils对象
private ConnectionUtils() {
}
private static ConnectionUtils connectionUtils = new ConnectionUtils();
//采用恶汉式单例设计模式,保证ConnectionUtils为单例
public static ConnectionUtils getInstance() {
return connectionUtils;
}
public Connection getCurConnection() throws SQLException {
//如果当前线程没有连接与其绑定,则从druid连接池中获取一个连接,并与当前线程绑定
if (threadLocal.get() == null) {
DruidPooledConnection connection = DruidUtils.getInstance().getConnection();
threadLocal.set(connection);
return connection;
} else {
//如果有连接,则直接返回与此线程绑定的连接
return threadLocal.get();
}
}
}
AccountDaoImpl中,将之前直接从连接池获取对象,改成通过工具类从线程中获取。注意的是,connection对象在用完后不要关闭。
public class AccountDaoImpl implements AccountDao {
@Override
public Account queryAcctByCardId(String cardId) throws Exception {
//使用阿里druid连接池,获取连接
// Connection connection = DruidUtils.getInstance().getConnection();
//修改为:从当前线程中获取connection
Connection connection = ConnectionUtils.getInstance().getCurConnection();
String sql = "select * from account where cardId = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,cardId);
ResultSet resultSet = preparedStatement.executeQuery();
Account account = new Account();
while (resultSet.next()){
account.setName(resultSet.getString("cardId"));
account.setMoney(resultSet.getInt("money"));
account.setCardId(resultSet.getString("cardId"));
}
//关闭连接,归还连接到连接池
resultSet.close();
preparedStatement.close();
//连接不可以关闭,否则下次调用的时候就又需要创建新的连接
// connection.close();
return account;
}
@Override
public int updateAcctByCardId(Account account) throws Exception {
//使用阿里druid连接池,获取连接
// Connection connection = DruidUtils.getInstance().getConnection();
//修改为:从当前线程中获取connection
Connection connection = ConnectionUtils.getInstance().getCurConnection();
String sql = "update account set money = ? where cardId = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1,account.getMoney());
preparedStatement.setString(2,account.getCardId());
int result = preparedStatement.executeUpdate();
//关闭连接,归还连接到连接池
preparedStatement.close();
//连接不可以关闭,否则下次调用的时候就又需要创建新的连接
// connection.close();
return result;
}
(2)在service层,添加事务
创建事务管理器,用于管理事务,还是说这里的事务归根结底还是connection的事务控制。
/**
* 事务管理器:用于手动管理事务的开启、提交、回滚
*/
public class TransactionManager {
private TransactionManager() {
}
private static TransactionManager transactionManager = new TransactionManager();
//饿汉式实现单例
public static TransactionManager getInstance() {
return transactionManager;
}
public void startTransaction() throws SQLException {
//关闭JDBC中自动事务提交
ConnectionUtils.getInstance().getCurConnection().setAutoCommit(false);
}
public void commit() throws SQLException {
ConnectionUtils.getInstance().getCurConnection().commit();
}
public void rollBack() throws SQLException {
ConnectionUtils.getInstance().getCurConnection().rollback();
}
}
在transfer中,我们可以再加一个finally(),在finally中进行连接的关闭以及threadLocal.remove()移除绑定的连接。
public class TransferServiceImpl implements TransferService {
// private AccountDao accountDao = new AccountDaoImpl();
//通过工厂获取对象
// private AccountDao accountDao = (AccountDaoImpl)BeanFactory.getBean("accountDao");
//最佳的方式
private AccountDao accountDao;
//set方法进行传值
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(String fromCardId, String toCardId, int money) throws Exception {
try {
//开启事务
TransactionManager.getInstance().startTransaction();
Account fromAcctInfo = accountDao.queryAcctByCardId(fromCardId);
Account toAcctInfo = accountDao.queryAcctByCardId(toCardId);
fromAcctInfo.setMoney(fromAcctInfo.getMoney() - money);
toAcctInfo.setMoney(toAcctInfo.getMoney() + money);
accountDao.updateAcctByCardId(fromAcctInfo);
int i = 1 / 0;
accountDao.updateAcctByCardId(toAcctInfo);
//提交事务
TransactionManager.getInstance().commit();
} catch (Exception e) {
//回滚事务
TransactionManager.getInstance().rollBack();
e.printStackTrace();
//抛出异常便于上层servlet捕获
throw e;
}
}
}
如果service中有几十个或者更多的方法,需要添加事务控制,每个方法都这样去写是不是太麻烦了?如何去解决呢?下一篇将采用动态代理的方式进行实现。