Skip to content

Java客户端 事务处理 基于DalCommand

He, Jiehui edited this page Apr 2, 2018 · 1 revision

简介

使用事务在携程这样规模的系统里面是普遍不推荐的。现在主流的开发方式是去事务,并通过其他手段保证数据的最终一致性。每次要使用事务之前应该先研究一下有没有非事务的替代解决方案。

我们希望把事务接口设计的尽可能方便使用,但我们也不鼓励用户滥用事务。因为滥用事务最终会导致系统性能大幅下降。

为了从设计上支持这种思想,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会有一个返回值,该值仅仅在调研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();
        }
    }

Transaction的提交和回滚

如果在单个command的执行中没有任何的exception出现,则会自动提交修改。否则回滚。对支持command list的处理也是一样,任何一个command出现excpetion,之前执行的command连同当前的都会回滚。否则全部提交。

Transaction提交和回滚的回调监听器

从版本1.6.0开始支持事务的回调

支持在Transaction提交和回滚的时候自动执行用户注册的监听器

public interface DalTransactionListener {
    void beforeCommit() throws SQLException;
    void beforeRollback();
    void afterCommit();
    void afterRollback();
}

Commit示例

@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();
    }
}

Rollback示例

@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) {
    }
}
Clone this wiki locally