jfinal多数据源的事务处理问题

项目里有这么一个需求场景

我们有多个系统,同时要共享一个积分数据,数据库的设计可能是这样的

业务系统数据库A

业务系统数据库B

积分资产数据库:同时存放这A系统和B系统的积分以及用户数据

这时候如果A系统做完一个业务处理后同时要对积分做增加或者减少也就是说在一个sevice中会调用2个数据库操作

serviceHandler {

    // 操作数据库A

   // 操作积分数据库

}

在这样的情况下如果操作数据库A成功了,但是操作数据库B失败了,如何回滚,我现在的回滚是这么去做的

@Aspect
@Component
public class JFinalTxAop {

    @Pointcut(value = "@annotation(com.mr.common.annotation.JFinalTx)")
    private void jFinalTx() {}

    @Around(value = "jFinalTx()", argNames = "pjp")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        Object retVal = null;
        Config config = getConfigWithTxConfig(pjp);
        if (config == null)
            config = DbKit.getConfig();

        Connection conn = config.getThreadLocalConnection();
        // Nested transaction support
        if (conn != null) {
            try {
                if (conn.getTransactionIsolation() < getTransactionLevel(config))
                    conn.setTransactionIsolation(getTransactionLevel(config));
                retVal = pjp.proceed();
                return retVal;
            } catch (SQLException e) {
                throw new ActiveRecordException(e);
            }
        }

        Boolean autoCommit = null;
        try {
            conn = config.getConnection();
            autoCommit = conn.getAutoCommit();
            config.setThreadLocalConnection(conn);
            // conn.setTransactionIsolation(transactionLevel);
            conn.setTransactionIsolation(getTransactionLevel(config));

            conn.setAutoCommit(false);
            retVal = pjp.proceed();
            conn.commit();
        } catch (NestedTransactionHelpException e) {
            if (conn != null) try {conn.rollback();} catch (Exception e1) {LogKit.error(e1.getMessage(), e1);}
            LogKit.logNothing(e);
        } catch (Throwable t) {
            if (conn != null) try {conn.rollback();} catch (Exception e1) {LogKit.error(e1.getMessage(), e1);}
            throw t instanceof RuntimeException ? (RuntimeException)t : new ActiveRecordException(t);
        }
        finally {
            try {
                if (conn != null) {
                    if (autoCommit != null)
                        conn.setAutoCommit(autoCommit);
                    conn.close();
                }
            } catch (Throwable t) {
                // can not throw exception here, otherwise the more important exception in previous catch block can not be thrown
                LogKit.error(t.getMessage(), t);
            }
            finally {
                // prevent memory leak
                config.removeThreadLocalConnection();
            }
        }
        return retVal;
    }

    /**
     * 获取配置的事务级别
     * @param config
     * @return
     */
    protected int getTransactionLevel(Config config) {
        return config.getTransactionLevel();
    }

    /**
     * 获取配置的TxConfig,可注解到class或者方法上
     * @param pjp
     * @return Config
     */
    public static Config getConfigWithTxConfig(ProceedingJoinPoint pjp) {
        MethodSignature ms = (MethodSignature) pjp.getSignature();
        Method method = ms.getMethod();
        TxConfig txConfig = method.getAnnotation(TxConfig.class);
        if (txConfig == null)
            txConfig = pjp.getTarget().getClass().getAnnotation(TxConfig.class);

        if (txConfig != null) {
            Config config = DbKit.getConfig(txConfig.value());
            if (config == null)
                throw new RuntimeException("Config not found with TxConfig: " + txConfig.value());
            return config;
        }
        return null;
    }
}

这段代码是我在jfianl论坛里找到的,但数据源的时候是有效的,原理很简单就是在处理一段业务的时候把事务打开,如果失败就回滚,成功就提交,但是我不确定这块在多数据源下是否有效,如果无效那么怎么去修改支持多数据源

还有个问题是,我如果不用多数据源的方式,我调用积分系统扣除或者增加系统用RPC或者HTTP去调用,那么如何保证我两个系统数据的一致性?

评论区

杜福忠

2022-12-02 18:00

我寻思直接写不就得了么?还是有什么梗没考虑到?
Db.use("A").tx(() -> Db.use("B").tx(() -> {
// 操作数据库A
// 操作数据库B(积分数据库)
return true;
}));

yjjdick1990

2022-12-02 20:33

@杜福忠 不会这种兰布达的写法还能这么写的啊,谢谢忠哥,我记得上面那个好像也是我抄的你的,还有代码生成器jfenjoy

yjjdick1990

2022-12-02 20:33

@杜福忠 如果是那种远程调用的方式我如何保证数据的一致性?

杜福忠

2022-12-02 23:37

@yjjdick1990 啊?不是我分享的吧。。。
jfenjoy是以前才学写的太繁琐了,3.3版后的Enjoy 就可以直接生成文件了。
分布式系统数据一致性网上资料挺多了,我没写过,估计大致就是让两边业务都操作成功了再双双提交事务吧。
https://jishuin.proginn.com/p/763bfbd5f959
以前有过一个业务是两套系统操作,但是当时是把业务拆到一个记录表,然后任务器专门执行那个业务数据,执行结果再存入记录表。用户通过接口定时循环查询记录表看结果。

杜福忠

2022-12-03 10:43

上面说的tx可能也有问题,当B事务提交成功了,A事务提交失败的时候,B的操作就执行了,也GG。。。还得学习其他的软件

杜福忠

2022-12-03 11:42

@yjjdick1990 https://www.bilibili.com/video/BV1Q4411y7ip?p=4&spm_id_from=pageDriver&vd_source=b4ceb381bd1cb6d4920343164c76e7c5

杜福忠

2022-12-03 13:09

@yjjdick1990 感觉业务简单的话,好像也可以用一下,
DbPro b = Db.use("B");
try {
Db.use("A").tx(() -> {
try {
return b.tx(() -> {
// 操作数据库A
// 操作数据库B(积分数据库)
return true;
});
} catch (Exception e) {
//log.err记录
return false;
}
});
}catch (Exception e){
//log.err记录
//b.update("sql");
//对数据库B的操作 进行删除或修改进行还原数据
}

yjjdick1990

2022-12-03 14:00

@杜福忠 感谢忠哥,我不知道3.3的enjoy可以直接生成文件了,你分享的那个我用了4~5年了。。。

JFinal

2022-12-04 13:49

多数据源事务属于分布式事务,jfinal 没有提供支持

你要实现分布式事务,最好是借助第三方中间件,专业的事情交给专业的人,用一用 Sharding JDBC 这类中间件,基本上只需要配置,对于应用来说是透明的

要保障分布式事务的原子性、一致性,不是一般人想象的那样简单,性能代价也比较大

一般可以在业务层想办法解决,例如两个数据源的事务分别提交,前面的事务提交后在数据库留下某种状态信息,根据状态再决定是否决定执行第二个事务,再根据第二个事务的成功与否再决定第一个事务的状态是否要回到原状态(需要业务代码)

上面的方式比较麻烦,还有一个简单办法是,在设计层面将两个事务拆分成两个状态,让其成为业务的流程的两个环节,根据状态可决定下一步可以做什么事情

有关业务流程的表中通常要放一个 state 状态字段

山东小木

2022-12-04 15:40

@JFinal JFinal支持一下Seata就可以 http://seata.io/zh-cn/

热门反馈

扫码入社