Spring 50例常见错误(十七)

文章整理来源:Spring编程常见错误50例_spring_spring编程_bean_AOP_SpringCloud_SpringWeb_测试_事务_Data-极客时间

案例42:事务的传播机制

        希望的结果是,当注册课程发生错误时,只回滚注册课程部分,保证学生信息依然正常,即希望在内层事务抛出了异常,但外层的数据能够保留。 而实际是内外层的数据都回滚了

// 外层事务-保存学生信息,并关联课程
@Service
public class StudentService {
  //省略非关键代码
  @Transactional(rollbackFor = Exception.class)
  public void saveStudent(String realname) throws Exception {
      Student student = new Student();
      student.setRealname(realname);
      studentService.doSaveStudent(student);
      try {
          courseService.regCourse(student.getId());
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
  //省略非关键代码
}
------------------------------------------------
// 内层事务-并关联课程
@Service
public class CourseService {
    @Autowired
    private CourseMapper courseMapper;

    @Autowired
    private StudentCourseMapper studentCourseMapper;

    // 注意这个方法标记了“Transactional”,并抛出了异常
    @Transactional(rollbackFor = Exception.class)
    public void regCourse(int studentId) throws Exception {
        studentCourseMapper.saveStudentCourse(studentId, 1);
        courseMapper.addCourseNumber(1);
        throw new Exception("注册失败");
    }
}

        解析:在 Spring 声明式的事务处理中,有一个属性 propagation,

        其中 propagation 有 7 种配置:REQUIREDSUPPORTSMANDATORYREQUIRES_NEWNOT_SUPPORTEDNEVERNESTED。默认是 REQUIRED,它的含义是:如果本来有事务,则加入该事务,如果没有事务,则创建新的事务。

        Spring 事务处理的核心,其关键实现参考 TransactionAspectSupport.invokeWithinTransaction(),该方法流程: 1. 检查是否需要创建事务;2. 调用具体的业务方法进行处理;3. 提交事务; 4. 处理异常。

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {
 
   TransactionAttributeSource tas = getTransactionAttributeSource();
   final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
   final PlatformTransactionManager tm = determineTransactionManager(txAttr);
   final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
   if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
      // 是否需要创建一个事务
      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
      Object retVal = null;
      try {
         // 调用具体的业务方法
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
         // 当发生异常时进行处理
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }
      // 正常返回时提交事务
      commitTransactionAfterReturning(txInfo);
      return retVal;
   }
   //......省略非关键代码.....
}

        而当前案例是两个事务嵌套的场景,外层事务 doSaveStudent() 和内层事务 regCourse(),每个事务都会调用到这个方法。所以,这个方法会被调用两次

        当内层的方法抛出异常时,会调用 TransactionAspectSupport.completeTransactionAfterThrowing() 进行异常处理。在方法中,会先通过 TransactionManager.rollback() 对异常的类型进行检测,并在 rollback 中继续调用 processRollback(),而这个方法里区分了三种不同类型的情况:1. 是否有保存点;2. 是否为一个新的事务;3. 是否处于一个更大的事务中

        默认的传播类型 REQUIRED,嵌套的事务并没有开启一个新的事务,所以在这种情况下,当前事务是处于一个更大的事务中,所以会走到情况 3  分支 1 的代码块下

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
   try {
      boolean unexpectedRollback = unexpected;

      if (status.hasSavepoint()) {
         // 有保存点
         status.rollbackToHeldSavepoint();
      }
      else if (status.isNewTransaction()) {
         // 是否为一个新的事务
         doRollback(status);
      }
      else {
        // 处于一个更大的事务中
        if (status.hasTransaction()) {
           // 分支1
           if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
              doSetRollbackOnly(status);
           }
        }
        if (!isFailEarlyOnGlobalRollbackOnly()) {
           unexpectedRollback = false;
        }
      }

      // 省略非关键代码 
      if (unexpectedRollback) {
         throw new UnexpectedRollbackException(
               "Transaction rolled back because it has been marked as rollback-only");
      }
   }
   finally {
      cleanupAfterCompletion(status);
   }
}

        这里有两个判断条件来确定是否设置为仅回滚:status.isLocalRollbackOnly() 和 isGlobalRollbackOnParticipationFailure() 满足任何一个,都会执行 doSetRollbackOnly() 操作。

        isLocalRollbackOnly 在当前的情况下是 false,所以是否分设置为仅回滚就由 isGlobalRollbackOnParticipationFailure() 这个方法来决定了,其默认值为 true, 即是否回滚交由外层事务统一决定 。显然这里的条件得到了满足,从而执行 doSetRollbackOnly

        到此内层事务的操作基本执行完毕,它处理了异常,并最终调用到了 DataSourceTransactionObject 中的 setRollbackOnly()

        而在外层事务中,代码不会捕获内层异常,因为内层先自行捕获了。最后的事务会在 TransactionAspectSupport.invokeWithinTransaction() 中的 commitTransactionAfterReturning() 中进行处理

protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
   if (txInfo != null && txInfo.getTransactionStatus() != null) {     txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
   }
}
-----------------------------------------------------
public final void commit(TransactionStatus status) throws TransactionException {
   //......省略非关键代码.....
   if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
      processRollback(defStatus, true);
      return;
   }

   processCommit(defStatus);
}
-------------------------------------------------------------------
public boolean isGlobalRollbackOnly() {
   return ((this.transaction instanceof SmartTransactionObject) &&
         ((SmartTransactionObject) this.transaction).isRollbackOnly());
}
------------------------------------------------------

public boolean isRollbackOnly() {
   return getConnectionHolder().isRollbackOnly();
}

        外层事务是否回滚的关键,最终取决于 DataSourceTransactionObject 类中的 isRollbackOnly(),而该方法的返回值,正是我们在内层异常的时候设置的

        isRollbackOnly() setRollbackOnly() 这两个方法的执行本质都是对 ConnectionHolder 中 rollbackOnly 属性标志位的存取,而 ConnectionHolder 则存在于 DefaultTransactionStatus 类实例的 transaction 属性之中

        所以,在内层 regCourse() 中抛出异常,并触发了回滚操作时,这个回滚会进一步传播,从而把外层 saveStudent() 也回滚了。最终导致整个事务都被回滚了

        解决:对传播属性进行修改,把内层的事务传播类型改成 REQUIRES_NEW

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void regCourse(int studentId) throws Exception {
    studentCourseMapper.saveStudentCourse(studentId, 1);
    courseMapper.addCourseNumber(1);
    throw new Exception("注册失败");
}

案例43:不同数据源之间使用事务

        对应学生注册和发卡使用不同是数据源,但在这需要让 注册 和 发卡 保持在同一个事务中

@Service
public class CardService {
    @Autowired
    private CardMapper cardMapper;

    @Transactional
    public void createCard(int studentId) throws Exception {
        Card card = new Card();
        card.setStudentId(studentId);
        card.setBalance(50);
        cardMapper.saveCard(card);
    }
}

        解析:在Spring 里有这样一个抽象类 AbstractRoutingDataSource,这个类相当于 DataSource 的路由中介,在运行时根据某种 key 值来动态切换到所需的 DataSource 上。

        通过实现这个类就可以实现期望的动态数据源切换。这个类里有这么几个关键属性:

        targetDataSources 保存了 key 和数据库连接的映射关系;

        defaultTargetDataSource 标识默认的连接;

        resolvedDataSources 存储数据库标识和数据源的映射关系。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

   @Nullable
   private Map<Object, Object> targetDataSources;

   @Nullable
   private Object defaultTargetDataSource;

   private boolean lenientFallback = true;

   private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

   @Nullable
   private Map<Object, DataSource> resolvedDataSources;

   @Nullable
   private DataSource resolvedDefaultDataSource;
 
   //省略非关键代码
}

        获取数据库连接的是 getConnection(),它调用了 determineTargetDataSource() 来创建连接。而 determineTargetDataSource() 是整个部分的核心,它的作用就是动态切换数据源。有多少个数据源,就存多少个数据源在 targetDataSources 中。

        选择哪个数据源又是由 determineCurrentLookupKey() 来决定的,此方法是抽象方法,需要继承 AbstractRoutingDataSource 抽象类来重写此方法。该方法返回一个 key,该 key 是 Bean 中的 beanName,并赋值给 lookupKey,由此 key 可以通过 resolvedDataSources 属性的键来获取对应的 DataSource 值,从而达到数据源切换的效果

@Override
public Connection getConnection() throws SQLException {
   return determineTargetDataSource().getConnection();
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
   return determineTargetDataSource().getConnection(username, password);
}
------------------------------------------------------

protected DataSource determineTargetDataSource() {
   Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
   Object lookupKey = determineCurrentLookupKey();
   DataSource dataSource = this.resolvedDataSources.get(lookupKey);
   if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
   }
   if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
   }
   return dataSource;
}

        解决:1. 首先创建一个 MyDataSource 类,继承了 AbstractRoutingDataSource,并覆写了 determineCurrentLookupKey():

public class MyDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> key = new ThreadLocal<String>();

    @Override
    protected Object determineCurrentLookupKey() {
        return key.get();
    }

    public static void setDataSource(String dataSource) {
        key.set(dataSource);
    }

    public static String getDatasource() {
        return key.get();
    }

    public static void clearDataSource() {
        key.remove();
    }
}

        2. 其次,需要修改 JdbcConfig。新写了一个 dataSource,将原来的 dataSource 改成 dataSourceCore,再将新定义的 dataSourceCore 和 dataSourceCard 放进一个 Map,对应的 key 分别是 core 和 card,并把 Map 赋值给 setTargetDataSources


public class JdbcConfig {
    //省略非关键代码
    @Value("${card.driver}")
    private String cardDriver;

    @Value("${card.url}")
    private String cardUrl;

    @Value("${card.username}")
    private String cardUsername;

    @Value("${card.password}")
    private String cardPassword;

    @Autowired
    @Qualifier("dataSourceCard")
    private DataSource dataSourceCard;

    @Autowired
    @Qualifier("dataSourceCore")
    private DataSource dataSourceCore;

    //省略非关键代码

    @Bean(name = "dataSourceCore")
    public DataSource createCoreDataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        return ds;
    }

    @Bean(name = "dataSourceCard")
    public DataSource createCardDataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(cardDriver);
        ds.setUrl(cardUrl);
        ds.setUsername(cardUsername);
        ds.setPassword(cardPassword);
        return ds;
    }

    @Bean(name = "dataSource")
    public MyDataSource createDataSource() {
        MyDataSource myDataSource = new MyDataSource();
        Map<Object, Object> map = new HashMap<>();
        map.put("core", dataSourceCore);
        map.put("card", dataSourceCard);
        myDataSource.setTargetDataSources(map);
        myDataSource.setDefaultTargetDataSource(dataSourceCore);
        return myDataSource;
    }

    //省略非关键代码
}

        3. 用 Spring AOP 来设置,把配置的数据源类型都设置成注解标签, Service 层中在切换数据源的方法上加上注解标签,就会调用相应的方法切换数据源。定义了一个新的注解 @DataSource,可以直接加在 Service() 上,实现数据库切换

@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value();

    String core = "core";

    String card = "card";
}
---------------------------------

@DataSource(DataSource.card)

        4.需要写一个 Spring AOP 来对相应的服务方法进行拦截,完成数据源的切换操作。特别要注意的是,这里要加上一个 @Order(1) 标记它的初始化顺序。这个 Order 值一定要比事务的 AOP 切面的值小,这样可以获得更高的优先级,否则自动切换数据源将会失效

@Aspect
@Service
@Order(1)
public class DataSourceSwitch {
    @Around("execution(* com.spring.puzzle.others.transaction.example3.CardService.*(..))")
    public void around(ProceedingJoinPoint point) throws Throwable {
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method.isAnnotationPresent(DataSource.class)) {
            DataSource dataSource = method.getAnnotation(DataSource.class);
            MyDataSource.setDataSource(dataSource.value());
            System.out.println("数据源切换至:" + MyDataSource.getDatasource());
        }
        point.proceed();
        MyDataSource.clearDataSource();
        System.out.println("数据源已移除!");
    }
}

        5. 最后实现了 Card 的发卡逻辑,在方法前声明了切换数据库,并在 saveStudent 中调用发卡逻辑

@Service
public class CardService {
    @Autowired
    private CardMapper cardMapper;
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @DataSource(DataSource.card)  // 切换数据源
    public void createCard(int studentId) throws Exception {
        Card card = new Card();
        card.setStudentId(studentId);
        card.setBalance(50);
        cardMapper.saveCard(card);
    }
}
------------------------------------------------------------

@Transactional(rollbackFor = Exception.class)
public void saveStudent(String realname) throws Exception {
    Student student = new Student();
    student.setRealname(realname);
    studentService.doSaveStudent(student);
    try {
        courseService.regCourse(student.getId());
        cardService.createCard(student.getId());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值