多线程和事务之Workaround

最近对多线程感兴趣了,感觉 JDK 5 的多线程 API 很好用,而且功能强大。我在项目中用多线程并行执行两个 SQL 语句来缩短页面响应时间,由于都是读数据,所以对有没有事务无所谓。但是企业级软件都是很注重事务的,而且我们都是用spring的,所以想能不能在一个事务里执行几个线程,并且任何一个线程失败,其他线程都要失败,尤其是线程里对数据库的操作语句。但是google很多,都说没办法在Spring定义的事务里控制多个线程。而且自己的练习也证明这个行不通。

于是我在想能不能在每个线程开始的时候就开启一个事务,也就是N个线程有N个事务,但是每个事务的提交就仿照 “两段式提交”:就是在执行 Commit 语句之前,所有的线程必须到达一个共同的障碍点,只有所有线程都到达这个障碍点后,每个线程才执行自己的 commit 语句,否则必须相互等待。

对于这个场景,JDK 5 的 CyclicBarrier 类正好 COME-IN-HANDY。通过定义一个 barrier,让先到达的线程到了这个 barrier 的时候都要等待那些没有到达的线程,都到达后每个线程才继续自己的未尽事宜。对应我这里这个“未尽事宜”就是一个 Commit 语句。

首先,在数据库我定义一个简单的表用于测试,表结构如下:


然后写一个Java类,这个Java类里面执行两个线程,每个线程里开启一个事务,并且线程都依赖一个 CyclicBarrier,通过这个 CyclicBarrier, 让所有线程完成各自的工作后互相等待,然后一起跨过这个障碍点,进入 Commit 。如果其中某个线程失败后,其他线程都要跟着失败。

在贴出代码前,有一个知识需要提前分享下,就是 CyclicBarrier.await() 方法只抛出 InterruptedException, BrokenBarrierException,如果在调用这个 await 方法前,我们的代码逻辑就出现问题了,比如抛出异常了,如何让其他线程也知道呢。这里 CyclicBarrier example: error handling 给出了解决方案,我也就拿来主义了。

好了,代码贴出:

package com.igate.connection.thread;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Multithread {
	public static void main(String[] args) {
		final String url = "jdbc:oracle:thin:@ ~ :test";
		final String usrNm = "TPO_APP";
		final String pwd = "Sk7_Walk3r";

		try {
			Class.forName("oracle.jdbc.driver.OracleDriver");
		} catch (ClassNotFoundException e1) {
			e1.printStackTrace();
		}
		
		final String sql = "insert into TPO.ALLEN_TEST values (?, ?)";

		final CyclicBarrier barrier = new CyclicBarrier(2, new Runnable() {

			public void run() {
				System.out.println("done!!!!!!!!!");
			}
		});

		ExecutorService exec = Executors.newFixedThreadPool(2);

		exec.submit(new Runnable() {

			public void run() {
				try {
					Connection conn = DriverManager.getConnection(url, usrNm, pwd);
					conn.setAutoCommit(false);
					
					PreparedStatement ps = conn.prepareStatement(sql);
					ps.setInt(1, 1);
					ps.setString(2, "allen");
					ps.execute();

					PreparedStatement ps2 = conn.prepareStatement(sql);
					ps2.setInt(1, 2);
					ps2.setString(2, "allen123456");
					ps2.execute();

					barrier.await();
					
					conn.commit();
				} catch (InterruptedException e) {
					System.out.println("InterruptedException - allen123456");
					e.printStackTrace();
				} catch (BrokenBarrierException e) {
					System.out.println("BrokenBarrierException - allen123456");
					e.printStackTrace();
				} catch (Throwable t) {
					System.out.println("Throwable t  :: " + t.getMessage());
					
					Thread.currentThread().interrupt();
					try {
						barrier.await();
					} catch (Exception e) {
						System.out.println("force other threads to be in exception");
					}
				}
			}
		});

		exec.submit(new Runnable() {

			public void run() {
				try {
					Connection conn2 = DriverManager.getConnection(url, usrNm, pwd);
					conn2.setAutoCommit(false);
					
					PreparedStatement ps = conn2.prepareStatement(sql);
					ps.setInt(1, 3);
					ps.setString(2, "jack");
					ps.execute();

					barrier.await();

					conn2.commit();
				} catch (InterruptedException e) {
					System.out.println("InterruptedException - jack");
					e.printStackTrace();
				} catch (BrokenBarrierException e) {
					System.out.println("BrokenBarrierException - jack");
					e.printStackTrace();
				} catch (Throwable t) {
					Thread.currentThread().interrupt();
					try {
						barrier.await();
					} catch (Exception e) {
						System.out.println("force other threads to be in exception AA");
					}
				}
			}
		});

		exec.shutdown();

	}
}

commit() 语句是在线程正常通过 barrier.await(); 后才能执行,对数据库的操作才能持久化。由于表 ALLEN_TEST 里的 MYNAME 字段只能容纳 7 个字符,所以

ps2.setString(2, "allen123456"); 
将导致数据库异常,并且由于对 Barrier 的控制,让两个线程都抛异常而没有执行 commit 语句,因此没有数据插入到数据库:


在修改程序让两个线程都能正常插入数据之前,我得指出我的代码中的一个严重问题:两个线程中的connection是个局部变量,进入异常块的时候,变量的生命周期结束了。所以你看我的代码中没有 rollback 语句,而且 jack 的那个线程虽然没有问题,但是也没有提交。### 关于这个问题的修改,见文章最底下的 09-18日的修改。微笑 [-Added @ 09-18-2013-]

现在将

ps2.setString(2, "allen123456"); 
改为
ps2.setString(2, "kate"); 
再执行:


成功了,两个线程的数据操作都被持久化了。

在对数据库事务要求不是非常高,也就是并发不是非常厉害的情况下,可以考虑多线程执行来降低页面响应时间,每个线程里只将有依赖的SQL语句放在一起,没有依赖关系的可以放在另一个线程里;还有一个好处就是使用SQL后,可以方便地设置 FetchSize,减少 round-trip 的次数, 这是存储过程没办法做到的。([-Added @ 09-15-2013-]这里做个更正:如果使用比如Oracle的存储过程,其中的OUT类型是SYS_REFCURSOR,那么在JDBC中通过(Result)CallableStatement.getObject(INDEX)可以得到这个CURSOR对象(类似指针),但是初始时这个ResultSet并不返回任何值,当第一次调用next()时才从数据库抓取Oracle的defaultRowPrefetch行数,如果Cursor返回的结果集行数多于这个设置。defaultRowPrefetch在getConnection之前设置,这个值是全局的,对所有查询有效,可被覆盖。)

-----------------------------------------------------------------------------------------------

这样的解决方案也是有问题(挑战)的,比如由于每个线程占用一个数据库连接,所以数据库的连接数要够用;还有就是多个线程开启多个事务,虽然借助 CyclicBarrier 他们都是一起提交或回滚,算是满足了事务的基本要求,但是由于线程的启动时间不可预测,所以在最先启动的线程和最后启动的线程这个时间段里,数据库的状态可能发生了变化。对于需要Transaction-Level Read Consistency(比如转账),以Oracle数据库为例,设置TRANSACTION_SERIALIZABLE或者TRANSACTION_READ-ONLY(如果事务里只有SELECT语句),那么这个方案就肯定不行了。但是前面也提到,可以将这些读一致性的SQL语句放在一个事务里,其他对读一致性要求不高,也就是 TRANSACTION_READ_COMMITED 就能满足要求的SQL放在另一个事务里。参考Oracle 文档: Data Concurrency and Consistency

==========================9/26/2013===========================

经过后期对Oracle文档的阅读(特别是Oracle事务的解析),我发现9/18/2013的修改已经日臻完善,达到了一个机器里的类分布式事务,通过CyclicBarrier ,实现了两段式提交。至此,完全没问题,可以放心使用。上面被划线的句子完全是在没有理解Oracle事务基础上的臆测,故删除。

==========================9/18/2013===========================

上面代码中的Connection是个局部变量问题,无法显示调用rollback函数,这会埋下潜在的危险。根据Oracle文档,应当明确调用commit/rollback语句,而不能指望connection的自身的特性(autoCommit / close)来变相达到提交 / 回滚事务。借助JDK 7 try-with-resources 对程序进行改进,新代码如下:

public class Multithread {
	public static void main(String[] args) {
		final String url = "jdbc:oracle:thin:@ ~ :test";
		final String usrNm = "TPO_APP";
		final String pwd = "Sk7_Walk3r";
		
		try {
			Class.forName("oracle.jdbc.driver.OracleDriver");
		} catch (ClassNotFoundException e1) {
			e1.printStackTrace();
		} 
			
		final String sql = "insert into TPO.ALLEN_TEST values (?, ?)";

		final CyclicBarrier barrier = new CyclicBarrier(2, new Runnable() {

			public void run() {
				System.out.println("done!!!!!!!!!");
			}
		});
		
		ExecutorService exec = Executors.newFixedThreadPool(2);		
		
		exec.submit(new Runnable() {

			public void run() {
				try {
					try(Connection conn = DriverManager.getConnection(url, usrNm, pwd)){
						conn.setAutoCommit(false);
						try(PreparedStatement ps = conn.prepareStatement(sql)){
							ps.setInt(1, 1);
							ps.setString(2, "allen");
							ps.addBatch();

							ps.setInt(1, 2);
							ps.setString(2, "allen123456");
							ps.addBatch();
							
							ps.executeBatch();
							
							barrier.await();
							
							conn.commit();
						} catch (InterruptedException e) {
							conn.rollback();
							System.out.println("InterruptedException - allen123456");					
						} catch (BrokenBarrierException e) {
							conn.rollback();
							System.out.println("BrokenBarrierException - allen123456");							
						}  catch (Throwable t) {
							conn.rollback();
							
							System.out.println("Throwable t  :: " + t.getMessage());
							
							Thread.currentThread().interrupt();
							try {
								barrier.await();
							} catch (Exception e) {
								System.out.println("force other threads to be in exception");
							}
						}							
					}
				} catch (SQLException e) {
					System.out.println("SQLException e  :: " + e.getMessage());
					
				}
			}				
		});

		exec.submit(new Runnable() {

			public void run() {
				try {
					try (Connection conn2 = DriverManager.getConnection(url, usrNm, pwd)) {
						conn2.setAutoCommit(false);
						try (PreparedStatement ps = conn2.prepareStatement(sql)) {
							ps.setInt(1, 3);
							ps.setString(2, "jack");
							ps.execute();

							barrier.await();

							conn2.commit();
						} catch (InterruptedException e) {
							conn2.rollback();
							System.out.println("InterruptedException - jack");							
						} catch (BrokenBarrierException e) {
							conn2.rollback();
							System.out.println("BrokenBarrierException - jack");							
						} catch (Throwable t) {
							conn2.rollback();

							System.out.println("Throwable t  :: " + t.getMessage());

							Thread.currentThread().interrupt();
							try {
								barrier.await();
							} catch (Exception e) {
								System.out.println("force other threads to be in exception AA");
							}
						}
					}
				} catch (SQLException e) {
					System.out.println("SQLException e  :: " + e.getMessage());
				}
			}
		});

		exec.shutdown();		
	}
	
}
改进两点:

  1. 通过 try-with-resources 我们可以做到Connection等AutoCloseable的对象都能安全被关闭。
  2. 在每个Catch 块中显示调用 rollback 回滚事务,事务安全性得到保证。

我觉得再改进的话,是从Connection Pool 中得到connection,可以用一个方法如 getConnection() 替换程序中的 DriverManager.getConnection(url, usrNm, pwd) 实现。



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值