-
Notifications
You must be signed in to change notification settings - Fork 447
Java客户端 事务处理 基于DalCommand
使用事务在携程这样规模的系统里面是普遍不推荐的。现在主流的开发方式是去事务,并通过其他手段保证数据的最终一致性。每次要使用事务之前应该先研究一下有没有非事务的替代解决方案。
我们希望把事务接口设计的尽可能方便使用,但我们也不鼓励用户滥用事务。因为滥用事务最终会导致系统性能大幅下降。
为了从设计上支持这种思想,DAL提供DalCommand接口来让用户封装事务逻辑。DalCommand设计为一个函数式接口。这种设计可以引导用户把事务相关代码从其业务类代码中抽取出来,以便单独维护从而提高代码质量。
DalCommand接口仅包含一个execute方法,用户只要把事务中要执行的逻辑放在该方法里即可:
/**
* Wrapper for all the transaction operation. All the actions executed in
* the execute method will be executed in one transaction.
* @author jhhe
*/
public interface DalCommand {
/**
* Execute in same local transaction
* @param client
* @return true if going to next command, false if stop and commit
* @throws SQLException for roll back
*/
boolean execute(DalClient client) throws SQLException;
}
事务代码可以包含任意的复杂数据库操作,这些操作既可以是直接调用生成的DAO方法,也可以调用传入的DalClient接口提供的方法,后者仅仅是作为一种方便的应急或替代手段,本身不推荐使用。
用户提供的事务代码无需关心事务的开始与结束,DAL框架会自动完成这些通用的事务性操作。如果事务执行中没有异常抛出,则事务会自动commit,否则会自动rollback。
调用事务的方式是将实现的DalCommand传入DalClient的execute方法里面。
/**
* Execute customized command in the transaction.
*
* @param command Callback to be executed in a transaction
* @param hints Additional parameters that instruct how DAL Client perform database operation.
* @throws SQLException when things going wrong during the execution
*/
void execute(DalCommand command, DalHints hints) throws SQLException;
为了支持事务逻辑的合理划分和聚合,防止过大,过长的事务代码,DAL支持DalCommand的组合执行方式。用户可以把一个大的事务逻辑通过不同的DalCommand分解为多个小的事务块,然后把它们放到一个list调用DalClient的execute方法,这样可以保证这些事务逻辑都在一个事务里面执行。
/**
* Execute list of commands in the same transaction. This is useful when you have several
* commands and you want to combine them in the flexible way.
*
* @param commands Container that holds commands
* @param hints Additional parameters that instruct how DAL Client perform database operation.
* @throws SQLException when things going wrong during the execution
*/
void execute(List<DalCommand> commands, DalHints hints) throws SQLException;
事务是很重的数据库操作,需要开发人员认真对待。推荐的做法是创建一个DalCommand的public顶层实现类,并将事务逻辑放入里面。
比如:
public class MyBizCommand implements DalCommand{
private MyBizDao dao;
private MyBizContext context;
public MyBizCommand(MyBizContext context) throws SQLException {
dao = new MyBizDao();
}
@Override
public boolean execute(DalClient client) throws SQLException {
try {
dao.insert(context.getPojoForInsert(), new DalHints());
dao.update(context.getPojoForUpdate(), new DalHints());
...
context.setValueForResponse(response);
dao.delete(context.getPojoForDelete(), new DalHints());
} catch (Throwable e) {
DalException.wrap("your message here.", e);
}
return false;
}
}
调用时可以这样:
private DalClient client;
public Xxxx() throws SQLException {
client = DalClientFactory.getClient(MY_BIZ_DN_NAME);
}
public void bizLogic(MyBizContext context) throws SQLException {
//your biz logic here
doSomethingHere();
client.execute(new MyBizCommand(context), new DalHints());
Response resp = context.getValueForResponse();
//your biz logic there
doSomethingTHere();
}
当然也可以将其实现为一个匿名类。注意Java对匿名类可以引用的参数有特殊限定,必须是final类型的。因此需要把传入和传出的参数引用都设置为final。如果对Java不太熟悉,不推荐这种做法。
private DalClient client;
public Xxxx() {
client = DalClientFactory.getClient(MY_BIZ_DN_NAME);
}
public void bizLogic(final MyBizRequest request, final BizResponse response) throws SQLException {
//your biz logic here
doSomethingHere();
client.execute(new DalCommand() {public boolean execute(DalClient client) throws SQLException {
try {
dao.insert(request.getPojoForInsert(), new DalHints());
dao.update(request.getPojoForUpdate(), new DalHints());
response.setValue(yourValue);
dao.delete(request.getPojoForDelete(), new DalHints());
} catch (Throwable e) {
DalException.wrap("your message here.", e);
}
return false;
}}, new DalHints());
MyBizValue v = response.getValue();
//your biz logic there
doSomethingTHere();
}
一个设计的质量是好是坏主要看其必要步骤和用户自身逻辑的比例。实现为匿名类的时候可以适当修改格式以达到缩减代码行的效果。比如最啰嗦的Dal事务写法,纯Dal相关代码大概6行:
dalClient.execute(new DalCommand() {
@Override
public boolean execute(DalClient dalClient) throws SQLException {
return true;
}
}, new DalHints());
如果用户的事务处理大大超过6行,那么这种设计就是合理的。而如果大量的事务都是很简单的逻辑,那么就不合理。Dal对事务复杂度的估计是前者。
其实这个代码完全可以这样写:
dalClient.execute(new DalCommand() {public boolean execute(DalClient dalClient) throws SQLException {
return true;
}}, new DalHints());
这样就只有3行。这也是一般匿名类的标准写法。
每个command会有一个返回值,该值仅仅在调研command list的时候有判读作用。为true则继续调用当前command后面的command。为false则结束提交当前的transaction,并忽略后面的剩余的command。
command里面既可以调用给定的DalClient,也可以调用已经生成的dao中的任意方法,前提是保证访问的是同一个数据库的表。
为了表达功能和设计理念,下面的DalCommand代码都是通过匿名类实现,这不代表DAL的推荐用法。推荐的做法还是将事务逻辑创建为一个正式的DalCommand顶层实现。
例子。单个command直接使用DalClient
DalClient client = DalClientFactory.getClient("dao_test");
DalCommand command = new DalCommand() { public boolean execute(DalClient client) throws SQLException {
String delete = "delete from Person where id > 2000";
String insert = "insert into Person values(NULL, 'bbb', 100, 'aaaaa', 100, 1, '2012-05-01 10:10:00',1)";
String update = "update Person set name='abcde' where id > 2000";
String[] sqls = new String[]{delete, insert, insert, insert, update};
System.out.println(client.batchUpdate(sqls, hints));
StatementParameters parameters = new StatementParameters();
DalHints hints = new DalHints();
client.update(delete, parameters, hints);
selectPerson(client);
return true;
}
};
DalHints hints = new DalHints();
client.execute(command, hints);
多command方式
DalClient client = DalClientFactory.getClient("dao_test");
List<DalCommand> cmds = new LinkedList<DalCommand>();
cmds.add(new DalCommand() { public boolean execute(DalClient client) throws SQLException {
String delete = "delete from Person where id > 2000";
String insert = "insert into Person values(NULL, 'bbb', 100, 'aaaaa', 100, 1, '2012-05-01 10:10:00')";
String update = "update Person set name='abcde' where id > 2000";
String[] sqls = new String[]{insert, insert, insert, update};
System.out.println(client.batchUpdate(sqls, hints));
StatementParameters parameters = new StatementParameters();
DalHints hints = new DalHints();
client.update(delete, parameters, hints);
selectPerson(client);
return true;
}
});
cmds.add(new DalCommand() { public boolean execute(DalClient client) throws SQLException {
selectPerson(client);
return false; // 返回false,则后面所有的command都不会被执行,当前事物直接commit
}
});
cmds.add(new DalCommand() { public boolean execute(DalClient client) throws SQLException {
selectPerson(client);
return true;
}
});
DalHints hints = new DalHints();
client.execute(cmds, hints);
DalCommand的execute方法缺省提供一个DalClient以供使用,但这不意味着该方法里面只能使用DalClient。只要是针对同一个逻辑数据库的操作,execute里面可以调用任意的基于同一个逻辑数据库的DAO。
例如: DalClient client = DalClientFactory.getClient(DATA_BASE); private CloggingAppGenDao dao = MysqlDaoFactory.getCloggingAppGenDao();
public AppGenDeleteAndInsertDao(){
}
public void DeleteAndInsert(final CloggingAppGen gen){
DalCommand command = new DalCommand() { public boolean execute(DalClient client) throws SQLException {
dao.delete(gen);
test();
dao.insert(gen);
return true;
}
};
try {
client.execute(command, hints);
} catch (SQLException e) {
e.printStackTrace();
}
}
如果在单个command的执行中没有任何的exception出现,则会自动提交修改。否则回滚。对支持command list的处理也是一样,任何一个command出现excpetion,之前执行的command连同当前的都会回滚。否则全部提交。
从版本1.6.0开始支持事务的回调
支持在Transaction提交和回滚的时候自动执行用户注册的监听器
public interface DalTransactionListener {
void beforeCommit() throws SQLException;
void beforeRollback();
void afterCommit();
void afterRollback();
}
@Test
public void testCommitListeners() {
final DalHints hints = new DalHints();
final DalTransactionListener testListener = new DalTransactionListener(){
@Override
public void beforeCommit() throws SQLException {
Assert.assertTrue(DalTransactionManager.isInTransaction());
DalClient c = DalClientFactory.getClient(DalTransactionManager.getLogicDbName());
c.query("SELECT 1", new StatementParameters(), new DalHints(), new DalResultSetExtractor<Object>() {
@Override
public Object extract(ResultSet rs) throws SQLException {
return null;
}
});
}
@Override
public void beforeRollback() {
fail();
}
@Override
public void afterCommit() {
Assert.assertFalse(DalTransactionManager.isInTransaction());
}
@Override
public void afterRollback() {
fail();
}
};
final DalTransactionListener testListener1 = new DalTransactionListener(){
@Override
public void beforeCommit() throws SQLException {
Assert.assertTrue(DalTransactionManager.isInTransaction());
DalCommand cmd = new DalCommand() {
@Override
public boolean execute(DalClient client) throws SQLException {
client.query("SELECT 1", new StatementParameters(), new DalHints(), new DalResultSetExtractor<Object>() {
@Override
public Object extract(ResultSet rs) throws SQLException {
return null;
}
});
return false;
}
};
DalClientFactory.getClient(DalTransactionManager.getLogicDbName()).execute(cmd, new DalHints());
}
@Override
public void beforeRollback() {
fail();
}
@Override
public void afterCommit() {
Assert.assertFalse(DalTransactionManager.isInTransaction());
}
@Override
public void afterRollback() {
fail();
}
};
try {
final DalTransactionManager test = new DalTransactionManager(getDalConnectionManager());
ConnectionAction<?> action = new ConnectionAction<Object>() {
public Object execute() throws Exception {
DalTransactionManager.register(testListener);
DalTransactionManager.register(testListener1);
return null;
}
};
action.operation = DalEventEnum.EXECUTE;
test.doInTransaction(action, hints);
} catch (Exception e) {
e.printStackTrace();
fail();
}
}
@Test
public void testRollbackListeners() {
final DalHints hints = new DalHints();
final DalTransactionListener testListener = new DalTransactionListener(){
@Override
public void beforeCommit() {
}
@Override
public void beforeRollback() {
Assert.assertTrue(DalTransactionManager.isInTransaction());
}
@Override
public void afterCommit() {
fail();
}
@Override
public void afterRollback() {
Assert.assertFalse(DalTransactionManager.isInTransaction());
}
};
final DalTransactionListener testListener1 = new DalTransactionListener(){
@Override
public void beforeCommit() throws SQLException {
throw new SQLException();
}
@Override
public void beforeRollback() {
Assert.assertTrue(DalTransactionManager.isInTransaction());
}
@Override
public void afterCommit() {
fail();
}
@Override
public void afterRollback() {
Assert.assertFalse(DalTransactionManager.isInTransaction());
}
};
try {
final DalTransactionManager test = new DalTransactionManager(getDalConnectionManager());
ConnectionAction<?> action = new ConnectionAction<Object>() {
public Object execute() throws Exception {
DalTransactionManager.register(testListener);
// The 2nd listener will cause transaction rollback
DalTransactionManager.register(testListener1);
return null;
}
};
action.operation = DalEventEnum.EXECUTE;
test.doInTransaction(action, hints);
fail();
} catch (Exception e) {
}
}