baomidou / dynamic-datasource

dynamic datasource for springboot 多数据源 动态数据源 主从分离 读写分离 分布式事务
https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611
Apache License 2.0
4.76k stars 1.2k forks source link

关于事务的问题想说一下 #383

Closed cqyisbug closed 3 years ago

cqyisbug commented 3 years ago

文档上说的不支持spring原生事务,说的不完全对。

首先说下为什么不支持Spring原生事务,因为切面执行顺序不一样,如果 @Transactional 注解先执行会调用getConnection方法,这时候我们的@Ds注解还没起作用呢,所以拿到的Connection会有问题

但是我们让@Ds这个注解先起作用就可以了。

最后这个问题完全可以用手动调用切换数据源的操作来让项目支持Spring的原生事务 实现应该挺简单的。

huayanYu commented 3 years ago

不支持原生事务 是写在两种事务模式旁边的。 主要指不能混用。(这里写的确实有点问题,后面改成不能混用) spring原生事务下只能切一次数据源。

支持spring原生事务那么简单? show me the code 。 我们那么多公司一线负责人,还有seata项目的核心贡献者。都没有一次性完成spring原生事务的支持,希望你来支持下。

任何人能做到支持原生事务,并且保证多数据源事务一致性,作者承诺奖赏500元。~

isscy commented 3 years ago

@huayanYu 可以更详细一些的介绍一下为什么不支持spring原生事务嘛

githubtome commented 3 years ago

使用@DSTransactional注解有一个bug,就是必须要指定数据源即(@DS("XXXXX")),如果不指定数据源,会报空指针异常。 原因:因为没有使用事务注解的时候,直接通过数据源获取连接Connection。使用事务注解的时候,获取连接Connection时, 先到Connection代理工厂查询是否存在该连接,此时,由于没有使用@DS注解,所以本地线程内的数据库路由键为null, ConnectionProxy connection = ConnectionFactory.getConnection(ds);--》ConnectionProxy connection = ConnectionFactory.getConnection(null);。 解决方法:AbstractRoutingDataSource#getConnection()添加空指针判断即可。

huayanYu commented 3 years ago

使用@DSTransactional注解有一个bug,就是必须要指定数据源即(@ds("XXXXX")),如果不指定数据源,会报空指针异常。 原因:因为没有使用事务注解的时候,直接通过数据源获取连接Connection。使用事务注解的时候,获取连接Connection时, 先到Connection代理工厂查询是否存在该连接,此时,由于没有使用@ds注解,所以本地线程内的数据库路由键为null, ConnectionProxy connection = ConnectionFactory.getConnection(ds);--》ConnectionProxy connection = ConnectionFactory.getConnection(null);。 解决方法:AbstractRoutingDataSource#getConnection()添加空指针判断即可。

新版本 3.4.0已修复

MrLiuGangQiang commented 3 years ago

不支持原生事务 是写在两种事务模式旁边的。 主要指不能混用。(这里写的确实有点问题,后面改成不能混用) spring原生事务下只能切一次数据源。

支持spring原生事务那么简单? show me the code 。 我们那么多公司一线负责人,还有seata项目的核心贡献者。都没有一次性完成spring原生事务的支持,希望你来支持下。

任何人能做到支持原生事务,并且保证多数据源事务一致性,作者承诺奖赏500元。~

这个我这边倒是实现了这个功能,不过没采用mybatis plus提供的东西,是自己实现的,我也没细看你们的内部代码!主要是重写了事务管理器,我把核心代码贴这,万一能赚500块呢

/**
 * 动态数据源事务工厂
 *
 * @author LiuGangQiang Create in 2021/07/13
 */
public class DynamicTransactionFactory extends SpringManagedTransactionFactory {
  @Override
  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    return new DynamicTransaction(dataSource);
  }
}

/**
 * 动态数据源管理器 支持事务
 *
 * @author LiuGangQiang Create in 2021/07/13
 */
public class DynamicTransaction implements Transaction {
  private final static Logger LOGGER = LoggerFactory.getLogger(DynamicTransaction.class);

  /**
   * 数据源
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private final DataSource dataSource;

  /**
   * 主数据源连接
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private Connection connection;

  /**
   * 是否开启事务
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private Boolean isConnectionTransactional;

  /**
   * 是否自动提交
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private Boolean autoCommit;

  /**
   * 主数据源标识
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private String identification;

  /**
   * 其他连接缓存
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private ConcurrentMap<String, Connection> connections;

  /**
   * 构造器
   *
   * @author LiuGangQiang Create in 2021/07/13
   * @param dataSource 数据源
   */
  public DynamicTransaction(DataSource dataSource) {
    Assert.notNull(dataSource, "No DataSource specified");
    this.dataSource = dataSource;
    this.identification = DynamicDataSource.MASTER;
    connections = new ConcurrentHashMap<>();
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#getConnection()
   */
  @Override
  public Connection getConnection() throws SQLException {
    /* 获取当前生效的数据源标识 */
    String current = DataSourceContext.getDataSource();
    /* 先打开默认连接 主要是获取事务及自动提交 不一定在使用 */
    openConnection();
    if (current.equals(this.identification)) {
      /* 如果是默认数据源则返回连接 */
      return this.connection;
    } else {
      /* 不是默认数据源,获取连接并设置属性 */
      if (!connections.containsKey(current)) {
        try {
          Connection conn = this.dataSource.getConnection();
          /* 自动提交属性和主数据源保持连接 */
          conn.setAutoCommit(this.autoCommit);
          connections.put(current, conn);
        } catch (SQLException ex) {
          throw new CannotGetJdbcConnectionException("could not get jdbc connection", ex);
        }
      }
      return connections.get(current);
    }
  }

  /**
   * 打开连接
   *
   * @author LiuGangQiang Create in 2021/07/16
   * @throws SQLException
   */
  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.doGetConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("jdbc connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by spring");
    }
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#commit()
   */
  @Override
  public void commit() throws SQLException {
    if (this.connection != null && this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("committing jdbc connection [" + this.connection + "]");
      }
      this.connection.commit();
      for (Connection conn : connections.values()) {
        conn.commit();
      }
    }
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#rollback()
   */
  @Override
  public void rollback() throws SQLException {
    if (this.connection != null && this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("rolling back jdbc connection [" + this.connection + "]");
      }
      this.connection.rollback();
      for (Connection conn : connections.values()) {
        conn.rollback();
      }
    }
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#close()
   */
  @Override
  public void close() throws SQLException {
    DataSourceUtils.releaseConnection(this.connection, this.dataSource);
    for (Connection conn : connections.values()) {
      DataSourceUtils.releaseConnection(conn, this.dataSource);
    }
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#getTimeout()
   */
  @Override
  public Integer getTimeout() throws SQLException {
    return null;
  }
}

完整代码可以访问 https://gitee.com/fist-team/cdqt-micro-cloud

huayanYu commented 3 years ago

不支持原生事务 是写在两种事务模式旁边的。 主要指不能混用。(这里写的确实有点问题,后面改成不能混用) spring原生事务下只能切一次数据源。 支持spring原生事务那么简单? show me the code 。 我们那么多公司一线负责人,还有seata项目的核心贡献者。都没有一次性完成spring原生事务的支持,希望你来支持下。 任何人能做到支持原生事务,并且保证多数据源事务一致性,作者承诺奖赏500元。~

这个我这边倒是实现了这个功能,不过没采用mybatis plus提供的东西,是自己实现的,我也没细看你们的内部代码!主要是重写了事务管理器,我把核心代码贴这,万一能赚500块呢

/**
 * 动态数据源事务工厂
 *
 * @author LiuGangQiang Create in 2021/07/13
 */
public class DynamicTransactionFactory extends SpringManagedTransactionFactory {
  @Override
  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    return new DynamicTransaction(dataSource);
  }
}
/**
 * 动态数据源管理器 支持事务
 *
 * @author LiuGangQiang Create in 2021/07/13
 */
public class DynamicTransaction implements Transaction {
  private final static Logger LOGGER = LoggerFactory.getLogger(DynamicTransaction.class);

  /**
   * 数据源
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private final DataSource dataSource;

  /**
   * 主数据源连接
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private Connection connection;

  /**
   * 是否开启事务
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private Boolean isConnectionTransactional;

  /**
   * 是否自动提交
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private Boolean autoCommit;

  /**
   * 主数据源标识
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private String identification;

  /**
   * 其他连接缓存
   *
   * @author LiuGangQiang Create in 2021/07/18
   */
  private ConcurrentMap<String, Connection> connections;

  /**
   * 构造器
   *
   * @author LiuGangQiang Create in 2021/07/13
   * @param dataSource 数据源
   */
  public DynamicTransaction(DataSource dataSource) {
    Assert.notNull(dataSource, "No DataSource specified");
    this.dataSource = dataSource;
    this.identification = DynamicDataSource.MASTER;
    connections = new ConcurrentHashMap<>();
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#getConnection()
   */
  @Override
  public Connection getConnection() throws SQLException {
    /* 获取当前生效的数据源标识 */
    String current = DataSourceContext.getDataSource();
    /* 先打开默认连接 主要是获取事务及自动提交 不一定在使用 */
    openConnection();
    if (current.equals(this.identification)) {
      /* 如果是默认数据源则返回连接 */
      return this.connection;
    } else {
      /* 不是默认数据源,获取连接并设置属性 */
      if (!connections.containsKey(current)) {
        try {
          Connection conn = this.dataSource.getConnection();
          /* 自动提交属性和主数据源保持连接 */
          conn.setAutoCommit(this.autoCommit);
          connections.put(current, conn);
        } catch (SQLException ex) {
          throw new CannotGetJdbcConnectionException("could not get jdbc connection", ex);
        }
      }
      return connections.get(current);
    }
  }

  /**
   * 打开连接
   *
   * @author LiuGangQiang Create in 2021/07/16
   * @throws SQLException
   */
  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.doGetConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("jdbc connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by spring");
    }
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#commit()
   */
  @Override
  public void commit() throws SQLException {
    if (this.connection != null && this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("committing jdbc connection [" + this.connection + "]");
      }
      this.connection.commit();
      for (Connection conn : connections.values()) {
        conn.commit();
      }
    }
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#rollback()
   */
  @Override
  public void rollback() throws SQLException {
    if (this.connection != null && this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("rolling back jdbc connection [" + this.connection + "]");
      }
      this.connection.rollback();
      for (Connection conn : connections.values()) {
        conn.rollback();
      }
    }
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#close()
   */
  @Override
  public void close() throws SQLException {
    DataSourceUtils.releaseConnection(this.connection, this.dataSource);
    for (Connection conn : connections.values()) {
      DataSourceUtils.releaseConnection(conn, this.dataSource);
    }
  }

  /**
   * @see org.apache.ibatis.transaction.Transaction#getTimeout()
   */
  @Override
  public Integer getTimeout() throws SQLException {
    return null;
  }
}

完整代码可以访问 https://gitee.com/fist-team/cdqt-micro-cloud

这个方案思路没啥问题, 不过很久以前就有PR了,已经closed了。 原因是和mybatis强耦合, 需要做的是不关系底层orm实现的通用方案。

cqyisbug commented 3 years ago

作者牛逼,此issue当我没提。

MrLiuGangQiang commented 3 years ago

作者牛逼,此issue当我没提。

如果你确实需要这个功能,可以参考我的实现,目前是实现了事务内多数据源的链式切换和支持事务批量回滚的功能,还是蛮好用的,不过跟作者说的一致是和mybatis耦合了,如果你不用mybatis了可能需要自己额外去实现

cqyisbug commented 3 years ago

作者牛逼,此issue当我没提。

如果你确实需要这个功能,可以参考我的实现,目前是实现了事务内多数据源的链式切换和支持事务批量回滚的功能,还是蛮好用的,不过跟作者说的一致是和mybatis耦合了,如果你不用mybatis了可能需要自己额外去实现

非常感谢。

BestQwerty commented 1 month ago

cdqt-micro-cloud

请问一下方便看下完整代码吗?仓库404了。