论坛首页 入门讨论版 Java

请教一下关于jdbc级事务的死锁问题

浏览 206 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
时间:2008-05-15
情况大概是这样的

表table_A,和表table_B,在分属不同的业务逻辑中会以不同顺序进行更新
比如

业务逻辑business_X:
getConnection;
update table_A;
update table_B;
commit or rollback;


业务逻辑business_Y:
getConnection;
update table_B;
update table_A;
commit or rollback;

测试代码我用几个线程并发进行顺序的控制,关键代码如下


public class TestRun  implements Callable<Object>{
	private String sql;
	private Connection conn;
	private long timeOut;
	public TestRun(String sql,Connection conn,long timeOut){
		this.sql=sql;
		this.timeOut=timeOut;
		this.conn=conn;
	}
	public Object call() throws Exception {
		Thread.currentThread().sleep(timeOut);
		//打印信息,可以无视
		System.out.println("start conn="+conn+" ,   sql='"+sql+"' ,   time="+((System.nanoTime()%100000000000L)));
		System.out.println("");
		try{
			conn.createStatement().executeUpdate(sql);
		}catch(Exception e){
			e.printStackTrace();
		}
		//打印信息,可以无视
		System.out.println("end conn="+conn+" ,   sql='"+sql+"' ,   time="+((System.nanoTime()%100000000000L)));
		System.out.println("");
		return null;
	}
}



业务逻辑的模拟代码关键部分如下
		Connection conn1=ConnectionPool.getConnection();
		Connection conn2=ConnectionPool.getConnection();

		ExecutorService  es=Executors.newFixedThreadPool(10);

		TestRun tr1=new TestRun("update table_A set field0='11111111' where id=1;",conn1,1000);
		TestRun tr2=new TestRun("update table_B set field0='22222222' where id=1;",conn2,3000);
		TestRun tr11=new TestRun("update table_B set field0='11111111' where id=1;",conn1,5000);
		TestRun tr22=new TestRun("update table_A set field0='22222222' where id=1;",conn2,7000);

		es.submit(tr1);
		es.submit(tr11);
		es.submit(tr2);
		es.submit(tr22);


最后打印的结果和异常如下

start conn=com.mysql.jdbc.ConnectionImpl@17ee8b8 ,   sql='update table_A set field0='11111111' where id=1;' ,   

time=23969656199

end conn=com.mysql.jdbc.ConnectionImpl@17ee8b8 ,   sql='update table_A set field0='11111111' where id=1;' ,   

time=23971563424

start conn=com.mysql.jdbc.ConnectionImpl@1995d80 ,   sql='update table_B set field0='22222222' where id=1;' ,   

time=25970551285

end conn=com.mysql.jdbc.ConnectionImpl@1995d80 ,   sql='update table_B set field0='22222222' where id=1;' ,   

time=25971987501

start conn=com.mysql.jdbc.ConnectionImpl@17ee8b8 ,   sql='update table_B set field0='11111111' where id=1;' ,   

time=27970671691

start conn=com.mysql.jdbc.ConnectionImpl@1995d80 ,   sql='update table_A set field0='22222222' where id=1;' ,   

time=29970794053

end conn=com.mysql.jdbc.ConnectionImpl@17ee8b8 ,   sql='update table_B set field0='11111111' where id=1;' ,   

time=29973001596

com.mysql.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting 

transaction
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1042)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:957)
	at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3376)
	at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3308)
	at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1837)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1961)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2537)
	at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1564)


end conn=com.mysql.jdbc.ConnectionImpl@1995d80 ,   sql='update table_A set field0='22222222' where id=1;' ,   

time=30003110730


	at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1485)
	at TestRun.call(TestRun.java:23)
	at TestRun.call(TestRun.java:1)
	at java.util.concurrent.FutureTask$Sync.innerRun(Unknown Source)
	at java.util.concurrent.FutureTask.run(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.lang.Thread.run(Unknown Source)



如果同样加上两个线程执行 commit() 代码后的结果是 conn1 的所有 sql 执行OK,结论应该是强制性的释放了 mysql 为 conn2 加的锁。

我的意思不是说Hibernate之类的解决方案,Hibernate只是JDBC的封装,说到底这个问题她也是需要面对和解决的。另外,业务逻辑上大锁一加,单线程处理也不用考虑了,效率低不说,把这些数据库级的死锁问题上升到业务层解决也决不是好办法。

希望熟悉JDBC数据库开发的同仁指点一下。我想,这类情况应该很常见才对,不知道通常应该怎么解决?
   
时间:2008-05-15
死锁确实是数据库锁定
但是你这样就说他是数据库级别................就过分了吧........

死锁完全是因为业务问题

再好的数据库 发两条SQL过去 该死锁还是死锁
   
0 请登录后投票
时间:2008-05-15
ddandyy 写道
死锁确实是数据库锁定
但是你这样就说他是数据库级别................就过分了吧........

死锁完全是因为业务问题

再好的数据库 发两条SQL过去 该死锁还是死锁


多谢多谢
呃……我说错了,应该不说是数据库级别,或者说,应该是dao层或者持久层关注的东西。
这种问题应该很常见,于是觉得JDBC应该提供了解决方案,比如,简单来说,我想当然的认为既然JDBC检测到了死锁问题,那么哪些资源冲突Connection应该清除,那么实际上Connection可以存储一次事务前几次的操作,然后“rollback”直到释放这些资源,再然后比如 wait(1000);redo()……
算了算了,自己去看看JDBC的API好了,要是实在没办法只能要么在业务层注意一下(这个貌似治标不治本),要么只好在DAO层给资源加锁(单线程效率……)或者……干脆无视,本次操作快速失败……
   
0 请登录后投票
时间:2008-05-15
如果真的那么简单......
就不会出现什么死锁了........

这个只能业务上注意 程序里注意......

P.S:运行一个DAO UPDATE的时候 居然会发给另一个DAO ROLLBACK命令 你不觉得诡异么
   
0 请登录后投票
时间:2008-05-15
ddandyy 写道
运行一个DAO UPDATE的时候 居然会发给另一个DAO ROLLBACK命令 你不觉得诡异么


貌似我想了想解决的办法就是如此……简单的改了一下模拟的测试用的业务逻辑,第一时间执行 sql1 ,隔4秒执行 sql2


public Object call() throws Exception {
		while(true){
			Thread.currentThread().sleep(timeOut);					try{
				conn.createStatement().executeUpdate(sql1);
			}catch(SQLException e){
				if(e.getErrorCode()==1213){
					try{
						conn.rollback();
						continue;
					}catch(Exception ex){
						ex.printStackTrace();
					}
				}			}			
			Thread.currentThread().sleep(4000);			
			try{
				conn.createStatement().executeUpdate(sql2);
			}catch(SQLException e){
				if(e.getErrorCode()==1213){
					try{
						conn.rollback();
						continue;
					}catch(Exception ex){
						ex.printStackTrace();
					}
				}
			}
			conn.commit();
			conn.close();
			return null;
		}		
	}



然后main函数里面调用如下:
		String sql1="update table_A set field0='11111111' where id=1;";
		String sql11="update table_B set field0='11111111' where id=1;";
		
		String sql2="update table_B set field0='22222222' where id=1;";
		String sql122="update table_A set field0='22222222' where id=1;";
		
		TestRun tr1=new TestRun(sql1,sql11,conn1,1000);
		TestRun tr2=new TestRun(sql2,sql122,conn2,3000);
		
		es.submit(tr1);
		es.submit(tr2);	

这样的顺序就是
第一秒 : conn1.exe(sql1)
第三秒 : conn2.exe(sql2)
第五秒 : conn1.exe(sql11)
第七秒 : conn2.exe(sql22)

当执行到第五秒 conn1.exe(sql11) 会等待 conn2 释放 table_B 资源,执行到 conn2.exe(sql22) 发现死锁,自动强制杀死 conn2 的所有操作,并抛出 SQLException ,打印和查资料看了看,死锁错误代码是1213,于是当检测到错误代码为1213时回滚conn2,然后再从头执行 conn2 的所有操作。实际代码会加上超时或者重试次数等来打断无限循环

测试是通过了,不过似乎也没从根本上解决问题带来:
1.时间片轮转,CPU时间的不可控:比如回滚conn2,立刻又执行conn2.exe(sql2),又死锁,没个准
2.e.getErrorCode()==1213这句代码实在恶心……鬼才知道其他数据库的死锁代码
3.效率问题,整个的回滚……代价太大,不过有选择的回滚,一步一savepoint,很傻很龌龊


ddandyy 写道
这个只能业务上注意 程序里注意......

这句话不是很赞成,总觉得会带来更多复杂性和不可控制性,比如第二个程序员进行系统升级,不通读第一个程序员的代码并排出所有这些会发生冲突的地方,恐怕这种问题的几率蛮大,而这些工作恐怕头痛得很


ddandyy 写道
如果真的那么简单......
就不会出现什么死锁了........

这句话超级赞成,之前没考虑这些问题,觉得似乎简单,现在回想起来,以前做的项目,碰上这种问题的几率应该有点大,居然都没注意到
   
0 请登录后投票
时间:2008-05-15
说老实话我觉得这么写很恶心...

如果每个操作都加这个判断就太傻了....
而如果做到共通的里面....这种自动化的东西 没准会出什么问题.......

我们是在业务上控制避免死锁的......

操作数据的时候先锁表...for update nowait 锁定之后再修改

而不是修改到一半再退回来
   
0 请登录后投票
论坛首页 入门讨论版 Java

跳转论坛:
JavaEye推荐