开发者社区> hresh> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

rollback-only异常令我对事务有了新的认识(一)

简介: rollback-only异常令我对事务有了新的认识(一)
+关注继续查看

背景

环境


相关环境配置:

  • SpringBoot+PostGreSQL
  • Spring Data JPA


问题


两个使用 Transaction 注解的 ServiceA 和 ServiceB,在 A 中引入了 B 的方法用于更新数据 ,当 A 中捕捉到 B 中有异常时,回滚动作正常执行,但是当 return 时则出现org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only异常。


代码示例:


ServiceA

@Transactional
public class ServiceA {
  @Autowired
  private ServiceB serviceB;
  public Object methodA() {
    try{
      serviceB.methodB();
    } catch (Exception e) {
      e.printStackTrace();
    }
    
    return null;
  }
}
复制代码


ServiceB


@Transactional
public class ServiceB {
  public void methodB() {
    throw new RuntimeException();
  }
}
复制代码


知识回顾


@Transactional


Spring Boot 默认集成事务,所以无须手动开启使用 @EnableTransactionManagement 注解,就可以用 @Transactional 注解进行事务管理。


@Transactional 的作用范围


  1. 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
  2. :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
  3. 接口 :不推荐在接口上使用。


@Transactional 的常用配置参数


1.jpg


关于事务传播机制的详细介绍,可以参考这篇文章


@Transactional 事务注解原理


@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。


如果一个类或者一个类中的 public 方法上被标注@Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。


Spring AOP 自调用问题


若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有@Transactional 注解的方法的事务会失效。

这是由于Spring AOP代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。

关于 AOP 自调用的问题,文章结尾会介绍相关解决方法。


@Transactional 的使用注意事项总结


  1. @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
  2. 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
  3. 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败。


Spring 的 @Transactional 注解控制事务有哪些不生效的场景?


  • 数据库引擎是否支持事务(MySQL的MyISAM引擎不支持事务);
  • 注解所在的类是否被加载成Bean类;
  • 注解所在的方法是否为 public 方法;
  • 是否发生了同类自调用问题;
  • 所用数据源是否加载了事务管理器;
  • @Transactional 的扩展配置 propagation(事务传播机制)是否正确。
  • 方法未抛出异常
  • 异常类型错误(最好配置rollback参数,指定接收运行时异常和非运行时异常)


案例分析


构建项目


1、创建 Maven 项目,选择相应的依赖。一般不直接用 MySQL 驱动,而选择连接池。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.6.RELEASE</version>
  <relativePath/> 
</parent>
<properties>
  <java.version>1.8</java.version>
  <mysql.version>8.0.19</mysql.version>
</properties>
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.18</version>
  </dependency>
</dependencies>
复制代码


2、配置 application.yml


spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mysql_db?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: none
    open-in-view: false
    properties:
      hibernate:
        order_by:
          default_null_ordering: last
        order_inserts: true
        order_updates: true
        generate_statistics: false
        jdbc:
          batch_size: 5000
    show-sql: true
logging:
  level:
    root: info # 是否需要开启 sql 参数日志
    org.springframework.orm.jpa: DEBUG
    org.springframework.transaction: DEBUG
    org.hibernate.engine.QueryParameters: debug
    org.hibernate.engine.query.HQLQueryPlan: debug
    org.hibernate.type.descriptor.sql.BasicBinder: trace
复制代码


  • hibernate.ddl-auto: update 实体类中的修改会同步到数据库表结构中,慎用。
  • show_sql 可开启 hibernate 生成的 SQL,方便调试。
  • open-in-view指延时加载的一些属性数据,可以在页面展现的时候,保持 session 不关闭,从而保证能在页面进行延时加载。默认为 true。
  • logging 下的几个参数用于显示 sql 的参数。


3、MySQL 数据库中创建两个表


CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `address` varchar(100) DEFAULT NULL,
  `created_date` timestamp NULL,
  `last_modified_date` timestamp NULL,
  PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
CREATE TABLE `job` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `user_id` bigint(20) NOT NULL,
  `address` varchar(100) DEFAULT NULL,
  `created_date` timestamp NULL,
  `last_modified_date` timestamp NULL,
  PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
复制代码


4、创建实体类并添加 JPA 注解

目前只创建两个简单的实体类,User 和 Job


@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@EqualsAndHashCode(of = "id")
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseDomain implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @CreatedDate
  private LocalDateTime createdDate;
  @LastModifiedDate
  private LocalDateTime lastModifiedDate;
}
@Entity
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class User extends BaseDomain {
  private String name;
  private Integer age;
  private String address;
  @OneToMany(cascade = CascadeType.ALL)
  @JoinColumn(name = "user_id")
  private List<Job> jobs = new ArrayList<>();
}
@Entity
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class Job extends BaseDomain {
  private String name;
  @ManyToOne
  @JoinColumn
  private User user;
  private String address;
}
复制代码


5、创建对应的 Repository

实现 JpaRepository 接口,生成基本的 CRUD 操作样板代码。并且可根据 Spring Data JPA 自带的 Query Lookup Strategies 创建简单的查询操作,在 IDEA 中输入 findBy 等会有提示。


@Repository
public interface UserRepository extends JpaRepository<User, Long> {
  List<User> findByAddress(String address);
  User findByName(String name);
  void deleteByName(String name);
}
@Repository
public interface JobRepository extends JpaRepository<Job, Long> {
  List<Job> findByUserId(Long userId);
}
复制代码


6、创建 UserService 及其实现类


public interface UserService {
  List<UserResponse> getAll();
  List<UserResponse> findByAddress(String address);
  UserResponse query(String name);
  UserResponse add(UserDTO userDTO);
  UserResponse update(UserDTO userDTO);
  void delete(String name);
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
  private final UserRepository userRepository;
  @Override
  public List<UserResponse> getAll() {
    List<User> users = userRepository.findAll();
    return users.stream().map(this::toUserResponse).collect(Collectors.toList());
  }
  @Override
  public List<UserResponse> findByAddress(String address) {
    List<User> users = userRepository.findByAddress(address);
    return users.stream().map(this::toUserResponse).collect(Collectors.toList());
  }
  @Override
  public UserResponse query(String name) {
    if (!Objects.equals("hresh", name)) {
      throw new RuntimeException();
    }
    User user = userRepository.findByName(name);
    return toUserResponse(user);
  }
  @Override
  public UserResponse add(UserDTO userDTO) {
    User user = User.builder().name(userDTO.getName())
        .age(userDTO.getAge()).address(userDTO.getAddress()).build();
    userRepository.save(user);
    return toUserResponse(user);
  }
  @Override
  public UserResponse update(UserDTO userDTO) {
    User user = userRepository.findByName(userDTO.getName());
    if (Objects.isNull(user)) {
      throw new RuntimeException();
    }
    user.setAge(userDTO.getAge());
    user.setAddress(userDTO.getAddress());
    userRepository.save(user);
    return toUserResponse(user);
  }
  @Override
  public void delete(String name) {
    userRepository.deleteByName(name);
  }
  private UserResponse toUserResponse(User user) {
    if (user == null) {
      return null;
    }
    List<Job> jobs = user.getJobs();
    List<JobItem> jobItems;
    if (CollectionUtils.isEmpty(jobs)) {
      jobItems = new ArrayList<>();
    } else {
      jobItems = jobs.stream().map(job -> {
        JobItem jobItem = new JobItem();
        jobItem.setName(job.getName());
        jobItem.setAddress(job.getAddress());
        return jobItem;
      }).collect(Collectors.toList());
    }
    return UserResponse.builder().name(user.getName()).age(user.getAge()).address(user.getAddress())
        .jobItems(jobItems)
        .build();
  }
}
复制代码


7、UserController


@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
  private final UserService userService;
  private final JobService jobService;
  @GetMapping
  public List<UserResponse> queryAll() {
    return userService.getAll();
  }
  @GetMapping("/address")
  public List<UserResponse> findByAddress(@RequestParam("address") String address) {
    return userService.findByAddress(address);
  }
  @GetMapping("/{name}")
  public UserResponse getByName(@PathVariable("name") String name) {
    return userService.query(name);
  }
  @PostMapping
  public UserResponse add(@RequestBody @Validated(Add.class) UserDTO userDTO) {
    return userService.add(userDTO);
  }
  @PutMapping
  public UserResponse update(@RequestBody @Validated(Update.class) UserDTO userDTO) {
    return userService.update(userDTO);
  }
  @DeleteMapping
  public void delete(@RequestParam(value = "name") @NotBlank String name) {
    userService.delete(name);
  }
  @PostMapping("/jobs")
  public void addJob(@RequestBody @Validated(Update.class) JobDTO jobDTO) {
    jobService.add(jobDTO);
  }
}
复制代码


最后来看一下整个项目的结构以及文件分布。


2.jpg


基于上述代码,我们将进行特定知识的学习演示。


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Python Django开发 异常及解决办法(一)
该错误表明views.py中没有return一个返回值给前端。
15 0
Python黑科技系列07-小项目-开发一个定制的小画板
有时候想得太多,只会让自己变得心态不好,白白浪费太多的时间,很没有必要。 因为很多时候,换一个角度,改一下心态,很多想不通的问题,瞬间就想通了。想不通就暂时先不想;不必纠结于一时。人随风过,自在花开花又落。越简单越幸福。
407 0
python scikit-learn计算tf-idf词语权重
  Python的scikit-learn包下有计算tf-idf的api,研究了下做个笔记 1 安装scikit-learn包 [python] view plain copy   sudo pip install scikit-learn   2 中文分词采用的jieba分词,安装jieba分词包
7406 0
Java项目导出源代码jar包在Eclipse中查看中文注释乱码的问题
问题现场: 由于项目比较多,全部添加到Maven主项目中的话虽然更新调试方便,但项目多了严重影响Eclipse的速度,所以将一部分项目单独导出包含源代码的jar包,上传到nexus上,供其它项目引用;但是在引用的项目中通过Eclipse查看时,中文注释都变成了乱码。
847 0
ANTLR v3
ANTLR v3 ANTLR是一个语法、语义解析的好工具。我两年没用这个工具了,今天意外的发现它变化很大。06年我曾经用ANTLR v2做过某个项目,之后再也没有用过。
911 0
DNGuard 企业版 v3.1 发布
Version 3.1.0 (December 11, 2008) [+] 增加对 IA64 平台的兼容. [+] 增加 ProtectUserString 辅助工具. [+] 使用DNGuard SDK,在加密后自动移除SDK属性.
741 0
探索Eclipse V3.1新特性
探索 Eclipse V3.1 的新特性 更高的可用性、更广泛的 Java 支持、更强的性能改善了 Eclipse 集成开发环境   对本教程的评价   帮助我们改进这些内容 级别: 中级 Martin Streicher, 主编, Linux Magazine 2006 年 4 月 03 日 这份教程演示了 Eclipse 的最新发行版 V3.1 中的许多新特性。
642 0
+关注
hresh
分享技术,记录人生
文章
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载