JDBC之事务的封装和Threadlocal实例

一、什么是事务?

         将多个操作当做一个原子操作来进行。(原子不可分割)

二、什么是ThreadLocal?

由以上API文档可以看出:

ThreadLocal主要用于解决线程安全问题,该类的对象一般以全局static形式存在,以便在全局范围内的资源共享,线程是安全的。ThreadLocal并不是本地线程,而是一个线程变量,它只是用来维护本地变量针对每个线程提供自己的变量版本,避免了多线程的冲突问题,每个线程只需要维护自己的版本就好,彼此独立,不影响到对方。下图可以看到,向ThreadLocal里面存东西就是创建了一个Map,一个线程对应一个Map集合,然后ThreadLocal把这个Map挂到当前的线程底下,一个key值对应一个value,这样Map就只属于当前线程。

在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

上面我们知道了变量副本的存放在了map中,当我们不在调用set,此时不在将引用指向该‘map’,而本线程退出时会执行资源回收操作,将申请的资源进行回收,其实就是将引用设置为null。这时已经不在有任何引用指向该map,故而会被垃圾回收。

对比ThreadLocal和Synchronized同步机制

相同点:都能解决多线程处理中对同一变量的访问冲突问题(即线程安全问题)。

不同点:

1、适用情况不同

Synchronized同步机制中,使用同步保证同一时间只有一个线程访问,多个线程不能同时访问共享资源,否则出错。而ThreadLocal隔离了相关资源,并在同一个线程中可以共享这个资源,不同线程彼此独立,修改不会影响对方。

2、实现效果不同

对于多线程资源共享问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同线程排队访问,后者为每个线程都提供了一个变量的副本,因此各个线程内部访问互不受影响。

三、jdbc如何控制事务?

         (1)jdbc在默认情况下自动提交事务,即一条sql语句执行后,立即提交事务。

         (2)如果多个操作(即有多条sql语句要执行)要当做一个整理来执行,比如:在转账业务中需要执行两个sql,一个用来对账号进行扣除余额的操作(-1000),另一个用来对另一个账号进行加余额操作(+1000),显然,两个操作要么同时成功,要么同时失败。

         (3)三个方法

          //当flag为false,表示禁止自动提交
          connection.setAutoCommit(booleanfalg);
          //提交事务
          connection.commit();
          //回滚事务
          connection.rollback();

四、事务的封装(实例)

         比如要“转账”操作,从资金账户(t_account表中的某条记录)扣掉200元,然后股票账户(t_stock表中的某条记录)增加价值200的股票。一般的编程逻辑是,先扣钱,紧接着再将和扣除的钱等价的股票数增加到指定股票账户。问题是,按照这种先后执行顺序,万一在扣钱后系统异常或者突然断电等极端情况下,可能会出现钱扣了但股票没买的情况,如何避免?需要将“转账”和“入账”这两个事务封装在一起,保证同时要么都执行,要么都不执行。示例如下:

        先创建两张数据表,资金账户和股票账户,"6221342156333300"账号中有1000元,股票账户中股票号为”600015“的数量为0;用到SQL语句:

create table t_account(
id int primary key auto_increment,
accountNo varchar(16),
balance int
) ENGINE=InnoDB;

insert into t_account(accountNo,balance) values('6221342156333300',1000);

create table t_stock(
id int primary key auto_increment,
stockNo varchar(6),
qty int
) ENGINE=InnoDB;

insert into t_stock(stockNo,qty)values('600015',0);
注:ENGINE=InnoDB;声明数据库支持事务,根据MySQL版本不同,有时也可写成type=InnoDB;

1.新建实体

/**
 * @Description:资金账户
 *
 */
public class Account {
	private int id;
	private String accountNo;
	private int balance;

	//省略get()和set()
}

/**
 * @Description:股票账户
 *
 */
public class Stock {
	private int id;
	private String stockNo;
	private int qty;

	//省略get()和set()
}
2.构造DAO,用来封装操作数据库的逻辑业务

public interface AccountDao {
	public Account findByAccountNo(String accountNo) throws Exception;

	public void modify(Account a) throws Exception;
}

public interface StockDao {
	public Stock findByStockNo(String stockNo) throws Exception;

	public void modify(Stock s) throws Exception;
}
3.实现DAO

public class AccountDAOImpl implements AccountDao {

    @Override
    public Account findByAccountNo(String accountNo) throws Exception {
        Connection conn = null;
        PreparedStatement stat = null;
        ResultSet rst = null;
        Account a = null;

        try {
            conn = DBUtil.getConnection2();
            stat = conn
                    .prepareStatement("select * from t_account where accountNo=?");
            stat.setString(1, accountNo);
            rst = stat.executeQuery();
            if (rst.next()) {
                a = new Account();
                a.setAccountNo(accountNo);
                a.setBalance(rst.getInt("balance"));
                a.setId(rst.getInt("id"));
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
        }

        return a;
    }

    @Override
    public void modify(Account a) throws Exception {
        Connection conn = null;
        PreparedStatement stat = null;
        try {
            conn = DBUtil.getConnection2();
            stat = conn.prepareStatement("update t_account set balance=? "
                    + "where accountNo=?");
            stat.setInt(1, a.getBalance());
            stat.setString(2, a.getAccountNo());
            stat.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
        }
    }

}

public class StockDaoImpl implements StockDao {

	@Override
	public Stock findByStockNo(String stockNo) throws Exception {
		Connection conn = null;
		PreparedStatement stat = null;
		ResultSet rst = null;
		Stock s = null;

		try {
			conn = DBUtil.getConnection2();
			stat = conn
					.prepareStatement("select * from t_stock where stockNo=?");
			stat.setString(1, stockNo);
			rst = stat.executeQuery();
			if (rst.next()) {
				s = new Stock();
				s.setStockNo(stockNo);
				s.setQty(rst.getInt("qty"));
				s.setId(rst.getInt("id"));
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
		}

		return s;
	}

	@Override
	public void modify(Stock s) throws Exception {
		Connection conn = null;
		PreparedStatement stat = null;
		try {
			conn = DBUtil.getConnection2();
			conn.setAutoCommit(false);// 禁止自动提交事务
			stat = conn.prepareStatement("update t_stock set qty=? "
					+ "where stockNo=?");
			stat.setInt(1, s.getQty());
			stat.setString(2, s.getStockNo());
			stat.executeUpdate();
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
		}
	}

}

4.设计工具类,利用ThreadLocal确保后续转账的“进账”和“出账”使用的是同一个Connection。

public class DBUtil {
	private static String driver = ConfigUtil.getValue("driver");
	private static String url = ConfigUtil.getValue("url");
	private static String dbUser = ConfigUtil.getValue("dbUser");
	private static String dbPwd = ConfigUtil.getValue("dbPwd");
	private static ThreadLocal<Connection> connectionHoders = new ThreadLocal<Connection>();

	public static synchronized Connection getConnection2() throws Exception {
		// 先从线程局部变量(看成一个容器)中取
		Connection conn = connectionHoders.get();
		if (conn == null) {
			conn = getConnection();
			// 以当前线程对象作为key,以conn作为value放到一个HashMap里面
			connectionHoders.set(conn);
		}
		return conn;
	}

	public static void close2() {
		// 以当前线程对象作为key,从HashMap中取对应的value
		Connection conn = connectionHoders.get();
		if (conn != null) {
			try {
				conn.close();
				connectionHoders.set(null);// 清空容器
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	public static Connection getConnection() throws Exception {
		Class.forName(driver);
		return DriverManager.getConnection(url, dbUser, dbPwd);
	}

	public static void close(Connection conn) {
		if (conn != null) {
			try {
				conn.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
}

5.这里使用了配置文件和配置工具类

db.properties:

#mysql
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test
dbUser=root
dbPwd=111111

#Oracle

AccountDao=dao.impl.AccountDAOImpl

/**
 * @Description:读取db.properties文件
 *
 */
public class ConfigUtil {

	public static Properties props = new Properties();

	static {
		InputStream ips = ConfigUtil.class.getClassLoader()
				.getResourceAsStream("util/db.properties");
		try {
			props.load(ips);
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("读取db.properties失败");
		}
	}

	public static String getValue(String key) {
		return props.getProperty(key);
	}
	
	public static void main(String[] args) {
		System.out.println(getValue("driver"));
	}
}

6.新建事务管理器,统一操作事务。

/**
 * @Description:事务管理器
 *
 */
public class TransactionManager {
	// 开始一个事务
	public static void begin() throws Exception {
		Connection conn = DBUtil.getConnection2();
		conn.setAutoCommit(false);
	}

	// 提交一个事务
	public static void commit() throws Exception {
		Connection conn = DBUtil.getConnection2();
		conn.commit();
		// 必须关闭连接
		DBUtil.close2();
	}

	// 回滚一个事务
	public static void rollback() throws Exception {
		Connection conn = DBUtil.getConnection2();
		conn.rollback();
		// 必须关闭连接
		DBUtil.close2();
	}
}

7.完成业务类,并设置异常测试。

/**
 * @Description:业务类
 *
 */
public class AccountService {
	public void buyStock(String accountNo, String stockNo, int amount)
			throws Exception {
		//开始事务
		TransactionManager.begin();

		try {
			AccountDao accountDao = new AccountDAOImpl();
			// 根据资金账户找到要操作的记录
			Account a = accountDao.findByAccountNo(accountNo);
			// 减去相应金额
			a.setBalance(a.getBalance() - amount);
			// 修改
			accountDao.modify(a);

			if (1 == 1) {
				throw new Exception("系统出现异常");
			}

			// 修改股票账户
			StockDao stockDao = new StockDaoImpl();
			Stock s = stockDao.findByStockNo(stockNo);
			s.setQty(s.getQty() + amount / 10);
			stockDao.modify(s);
			TransactionManager.commit();
		} catch (Exception e) {
			e.printStackTrace();
			TransactionManager.rollback();
		}

	}
}

8.使用单元测试测试7中的服务。

public class AccountServiceTest {

	@Test
	public void test() throws Exception {
		AccountService service = new AccountService();
		service.buyStock("6221342156333300", "600015", 10);
	}

}

运行效果图:

就算出现异常,也不会只执行异常前面的“扣钱”操作而不执行异常后面的“增股票”操作,而是两者都不执行。

若将7中

if (1 == 1)
改成
if (1 == 2)
即不抛出异常,此时

购买股票成功,扣钱和增股票同时进行!


源代码:

http://download.csdn.net/detail/daijin888888/9473339


转载请注明出处:

http://blog.csdn.net/daijin888888/article/details/50988053


  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值