本节内容
数据库读写中的事务管理,是我之前不怎么接触的部分,本节主要通过一个简单的转账的功能来理解一下事务管理。
代码展示
我们在之前项目的代码基础上,改写AccountService代码实现一个转账的功能。
首先在接口中添加transfer方法,然后在Impl进行实现,自此我们也可以知道为什么要分为Service层和Dao两个层,service中的方法具有业务属性,而Dao中只是对数据库里面对记录进行操作并不具有业务属性。
@Service(value="accountService")
public class AccountServiceImpl implements IAccountService {
@Resource(name = "accountDao")
private IAccountDao accountDao;//在Dao里面实现数据库的配置以及相关的读写操作,通过一个QueryRunner queryRunner这个对象进行,在程序执行的时候会找到这个bean并使用其中的操作方法。
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
public void transfer(int idFrom, int idTo, float money){
//先查找到相关的账户
Account fromAccount = accountDao.findAccountById(idFrom);
Account toAccount = accountDao.findAccountById(idTo);
#更新金额
fromAccount.setMoney(fromAccount.getMoney()-money);
toAccount.setMoney(toAccount.getMoney()+money);
#更新相应的数据库
accountDao.updateAccount(fromAccount);
accountDao.updateAccount(toAccount);
}
}添加完了之后,我们进行运行:
public class Client {
public static void main(String[] args){
AnnotationConfigApplicationContext ap = new AnnotationConfigApplicationContext(SpringConfigration.class);
AccountServiceImpl ac = ap.getBean("accountService", AccountServiceImpl.class);
ac.transfer(2,3,100);
ap.close();
}
}可以看到数据库里面的数据得到了正常的更新。
mysql> select * from account; +----+------+-------+ | id | name | money | +----+------+-------+ | 2 | bbb | 900 | | 3 | ccc | 1100 | | 4 | eeee | 3333 | | 5 | ppp | 100 | | 6 | ppp | 100 | | 7 | ppp | 100 | +----+------+-------+
事务问题
但是这里面有一个问题,如果我把transfer中手动插入一个异常
public void transfer(int idFrom, int idTo, float money){
//先查找到相关的账户
Account fromAccount = accountDao.findAccountById(idFrom);
Account toAccount = accountDao.findAccountById(idTo);
#更新金额
fromAccount.setMoney(fromAccount.getMoney()-money);
toAccount.setMoney(toAccount.getMoney()+money);
#更新相应的数据库
accountDao.updateAccount(fromAccount);
accountDao.updateAccount(toAccount);
}再次查询就发现,账户id为2的用户金额减少了,到那时id=3的用户由于发生了异常并没有得到更新,因此就出现了不一致的事务。
mysql> select * from account; +----+------+-------+ | id | name | money | +----+------+-------+ | 2 | bbb | 800 | | 3 | ccc | 1100 | | 4 | eeee | 3333 | | 5 | ppp | 100 | | 6 | ppp | 100 | | 7 | ppp | 100 | +----+------+-------+
为了对这个问题进行改善,我们需要将两个操作放在一个事务中进行,要失败同时失败,要成功那么也需要同时成功。
优化方法
对与这个问题我们知道我们配置对QueryRunner是prototype队列类型,因此在tranfer中会有不同对链接执行数据库对操作,而我们要做对就是要将这四个操作放在一次事务中进行执行,要么成功,要么失败。我们采用对做法是使用ThreadLocal来进行解决。
public void transfer(int idFrom, int idTo, float money){
//先查找到相关的账户
Account fromAccount = accountDao.findAccountById(idFrom); //会获取一个链接
Account toAccount = accountDao.findAccountById(idTo);//获取另外一个链接
#更新金额
fromAccount.setMoney(fromAccount.getMoney()-money);
toAccount.setMoney(toAccount.getMoney()+money);
#更新相应的数据库
accountDao.updateAccount(fromAccount);//获取再另外一个链接
accountDao.updateAccount(toAccount);//获取再再另外一个链接
}使用ThreadLocal对数据库对链接进行管理,也就是说将链接和线程绑定,同一个线程用的一定是相同的链接,然后我们要自己写一个事务管理类,针对一个链接进行事务的开启,提交,回滚和释放。
我们需要新增两个类:ConnectionUtil和TxManager,然后修改AccountServiceImpl和AccountDaoImpl中的代码,因为以前QueryRunner会自动化获取链接,而本次我们希望它使用的链接对一个线程来说是固定,所以需要适配ConnectionUtil。
代码修改
package com.lujuan.util;
import javax.sql.DataSource;
import java.sql.Connection;
/**
* @项目: spring-learn-demo
* @描述: 连接池管理,主要是把数据库对链接和线程进行绑定,保证同一个线程使用的是同一个链接
* @作者: lujuan03
* @创建日期: 2020-04-11
**/
public class ConnectionUtil {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
private DataSource dataSource;
public Connection getThreadConnection(){
try {//先尝试获取
Connection conn = tl.get();
if (conn == null) {
conn = dataSource.getConnection();
tl.set(conn);
}
return conn;
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void remove(){
tl.remove();
}
}然后是事务管理类:
package com.lujuan.util;
/**
* @项目: spring-learn-demo
* @描述: 事务管理:通过connection进行的事务管理
* @作者: lujuan03
* @创建日期: 2020-04-11
**/
public class TxManager {
private ConnectionUtil connectionUtil;
public void setConnectionUtil(ConnectionUtil connectionUtil) {
this.connectionUtil = connectionUtil;
}
//开启事务
public void beginTx(){
try{
this.connectionUtil.getThreadConnection().setAutoCommit(false);
}catch (Exception e){
e.printStackTrace();
}
}
//提交事务
public void commitTx(){
try{
this.connectionUtil.getThreadConnection().commit();
}catch (Exception e){
e.printStackTrace();
}
}
//回滚
public void rollbackTx(){
try{
this.connectionUtil.getThreadConnection().rollback();
}catch (Exception e){
e.printStackTrace();
}
}
//释放链接
public void releaseTx(){
try{
this.connectionUtil.getThreadConnection().close();
this.connectionUtil.remove();
}catch (Exception e){
e.printStackTrace();
}
}
}
然后是我们service层代码和dao层代码的修改
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Resource(name = "queryRunner")
private QueryRunner queryRunner;//这是commons.dbutils提供的
public void setQueryRunner(QueryRunner queryRunner) {
this.queryRunner = queryRunner;
}
@Resource(name = "connectionUtil")
private ConnectionUtil connectionUtil;
public void setConnectionUtil(ConnectionUtil connectionUtil) {
this.connectionUtil = connectionUtil;
}
public List<Account> findAllAccount() {
List<Account> reV = null;
try{
//查询之后顺便映射为实体类
reV = queryRunner.query(connectionUtil.getThreadConnection(),"select * from account", new BeanListHandler<Account>(Account.class));
}catch (Exception e){
throw new RuntimeException(e);
}
return reV;
}
public Account findAccountById(Integer id) {
Account reV = null;
try{
reV = queryRunner.query(connectionUtil.getThreadConnection(), "select * from account where id = ?", new BeanHandler<Account>(Account.class), id);
}catch (Exception e){
throw new RuntimeException(e);
}
return reV;
}
public void addAccount(Account account) {
try{
queryRunner.update(connectionUtil.getThreadConnection(), "insert into account(name, money) values (?, ?)", account.getName(), account.getMoney());
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void deleteAccountById(Integer id) {
try{
queryRunner.update(connectionUtil.getThreadConnection(), "delete from account where id = ?", id);
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void updateAccount(Account account) {
try{
queryRunner.update(connectionUtil.getThreadConnection(), "update account set name= ?, money = ? where id = ?", account.getName(), account.getMoney(), account.getId());
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
service层
@Service(value="accountService")
public class AccountServiceImpl implements IAccountService {
@Resource(name = "accountDao")
private IAccountDao accountDao;
@Resource(name = "txManager")
private TxManager txManager;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
public void transfer(int idFrom, int idTo, float money){
try{
//开启事务
txManager.beginTx();
//执行操作
Account fromAccount = accountDao.findAccountById(idFrom);
Account toAccount = accountDao.findAccountById(idTo);
fromAccount.setMoney(fromAccount.getMoney()-money);
toAccount.setMoney(toAccount.getMoney()+money);
accountDao.updateAccount(fromAccount);
//int a = 1/0;
accountDao.updateAccount(toAccount);
//提交事务
txManager.commitTx();
}catch (Exception e){
txManager.rollbackTx();
}finally {
txManager.releaseTx();
}
}
public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}
public Account findAccountById(Integer id) {
return accountDao.findAccountById(id);
}
public void addAccount(Account account) {
accountDao.addAccount(account);
}
public void deleteAccountById(Integer id) {
accountDao.deleteAccountById(id);
}
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
public void saveAccount() {
System.out.println("AccountServiceImpl: saveAccount");
}
@PostConstruct
public void init(){
System.out.println("init......");
}
@PreDestroy
public void destroy(){
System.out.println("destroy......");
}
}其余的改动就是要把txManager和connectionUtil这两个bean对象在xml文件中进行配置。然后我们执行tranfer方法,就发现转账即使在异常发生的情况下数据库里面的数据也是正常的。
只是在这里我们看到如果对所有的操作都开启事务管理,那么将有很多胶水代码,在接下来我们将学习如何移除这些胶水代码,使用更加又没的方法进行事务管理。



京公网安备 11010502036488号