SpringBoot2.x系列教程68--Spring Boot+JPA+Jta-atomikos實現分佈式事務

SpringBoot2.x系列教程68--Spring Boot+JPA+Jta-atomikos實現分佈式事務

作者:一一哥

我在上一章節中介紹了分佈式事務的相關理論知識,本章節中我會在SpringBoot中結合JPA,整合jta-atomikos來實現多數據源環境下的分佈式事務。

一. 多數據源環境下分佈式事務代碼實現

代碼環境:

  • Spring Boot2.2.5
  • mysql-connector-java8.0.11

創建數據庫對應的表

db1數據庫中的goods表:

db4數據庫中的user表:

1. 創建web項目

我們按照之前的經驗,創建一個web程序,並將之改造成Spring Boot項目,具體過程略。

2. 添加依賴包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</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>8.0.11</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.12</version>
</dependency>

要注意mysql-connector-java的版本不要太高,否則可能會產生一些異常。

3. 創建application.yml配置文件

spring:
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
    database-platform: org.hibernate.dialect.MySQL57Dialect

4. 創建兩個數據源對應的實體類

db1數據源的實體類Goods

package com.yyg.boot.entity.db1;

import lombok.Data;

import javax.persistence.*;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/3
 * @Description db1中的商品表
 */
@Entity
@Table(name = "goods")
@Data
public class Goods {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    private String name;

}

db4數據源的實體類User

package com.yyg.boot.entity.db2;

import lombok.Data;
import lombok.ToString;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/3
 * @Description db4中的用戶表
 */
@Entity
@Table(name = "user")
@Data
@ToString
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Column
    private String username;

    @Column
    private Date birthday;

    @Column
    private String sex;

    @Column
    private String address;

}

5. 創建2個數據源倉庫類

db1數據源倉庫類GoodsRepository

package com.yyg.boot.dao.db01;

import com.yyg.boot.entity.db1.Goods;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/3
 * @Description Description
 */
@Repository
public interface GoodsRepository extends JpaRepository<Goods, Long>,JpaSpecificationExecutor<Goods> {
}

db4數據源倉庫類UserRepository

package com.yyg.boot.dao.db02;

import com.yyg.boot.entity.db2.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/3
 * @Description Description
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long>,JpaSpecificationExecutor<User> {
}

6. 配置數據源及事務管理器等

這裏的配置是最關鍵的,一般代碼出問題都是因爲這裏有問題。

這裏針對兩個數據庫,分別設置兩個數據源goodsDataSource與userDataSource。這裏的DataSource必須是XADataSource類型的數據源才行,並且利用AtomikosDataSourceBean講數據源配置進去。

還有分別設置實體類管理工廠goodsEntityManagerFactory與userEntityManagerFactory。

再分別設置事務管理器goodsTransactionManager與userTransactionManager。

還要創建一個用來整合多個事務管理器的JtaTransactionManager,負責回滾到多個數據源中。

但要注意區分是否是Primary配置。

db1數據源配置類GoodsDataSourceConfig

package com.yyg.boot.config;

import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Objects;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/18
 * @Description 商品數據源配置類
 */
@Configuration
@EnableJpaRepositories(basePackages = {"com.yyg.boot.dao.db01"}, entityManagerFactoryRef = "goodsEntityManagerFactory", transactionManagerRef = "goodsTransactionManager")
public class GoodsDataSourceConfig {

    @Resource
    private JpaProperties jpaProperties;

    /**
     * 第一個數據源
     */
    @Primary
    @Bean(name = "goodsDataSource")
    public DataSource goodsDataSource() {
        DruidXADataSource druidXADataSource = new DruidXADataSource();
        druidXADataSource.setUrl("jdbc:mysql://localhost:3306/db1?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC");
        druidXADataSource.setUsername("root");
        druidXADataSource.setPassword("syc");
        druidXADataSource.setDefaultAutoCommit(false);
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(druidXADataSource);
        atomikosDataSourceBean.setUniqueResourceName("goodsDataSource");
        atomikosDataSourceBean.setPoolSize(5);

        return atomikosDataSourceBean;
    }

    /**
     * 商品實體類管理工廠
     */
    @Primary
    @Bean(name = "goodsEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean goodsEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(goodsDataSource())
                .properties(jpaProperties.getProperties())
                //設置實體類所在位置:類或包
                .packages("com.yyg.boot.entity.db1")
                //持久化單元名稱
                .persistenceUnit("goodsPersistenceUnit")
                .build();
    }

    /**
     * 商品事務管理器
     */
    @Primary
    @Bean(name = "goodsTransactionManager")
    public PlatformTransactionManager goodsTransactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(Objects.requireNonNull(goodsEntityManagerFactory(builder).getObject()));
    }

}

db4數據源配置類GoodsDataSourceConfig

package com.yyg.boot.config;

import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Objects;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/18
 * @Description 商品數據源配置類
 */
@Configuration
@EnableJpaRepositories(basePackages = {"com.yyg.boot.dao.db02"},entityManagerFactoryRef = "userEntityManagerFactory",transactionManagerRef = "userTransactionManager")
public class UserDataSourceConfig {

    /**
     * 自動注入jpa配置
     */
    @Resource
    private JpaProperties jpaProperties;

    @Bean(name = "userDataSource")
    public DataSource db4DataSource() {
        DruidXADataSource druidXADataSource = new DruidXADataSource();
        druidXADataSource.setUrl("jdbc:mysql://localhost:3306/db4?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC");
        druidXADataSource.setUsername("root");
        druidXADataSource.setPassword("syc");
        druidXADataSource.setDefaultAutoCommit(false);

        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(druidXADataSource);
        atomikosDataSourceBean.setUniqueResourceName("userDataSource");
        atomikosDataSourceBean.setPoolSize(5);

        return atomikosDataSourceBean;
    }

    /**
     * 將數據源、連接池、以及其他配置策略進行封裝返回給事務管理器
     * 自動裝配時當出現多個Bean候選者時,被註解爲@Primary的Bean將作爲首選者,否則將拋出異常
    */
    @Bean(name = "userEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean userEntityManagerFactory(EntityManagerFactoryBuilder builder){
      return builder.dataSource(db4DataSource())
              .properties(jpaProperties.getProperties())
              //設置實體類所在位置:類或包
              .packages("com.yyg.boot.entity.db2")
              //持久化單元名稱
              .persistenceUnit("userPersistenceUnit")
              .build();
    }

    /**
      * 返回數據源的事務管理器
      */
    @Bean(name = "userTransactionManager")
    public PlatformTransactionManager userTransactionManager(EntityManagerFactoryBuilder builder){
        return new JpaTransactionManager(Objects.requireNonNull(userEntityManagerFactory(builder).getObject()));
    }

}

創建用來整合多數據源的事務管理器JtaTransactionManager

package com.yyg.boot.config;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.UserTransaction;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/19
 * @Description Description
 */
@Configuration
public class JtaTransactionManagerConfig {

    @Primary
    @Bean(name = "jtaTransactionManager")
    public JtaTransactionManager regTransactionManager () {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        UserTransaction userTransaction = new UserTransactionImp();
        return new JtaTransactionManager(userTransaction, userTransactionManager);
    }

}

7. 創建Service及其實現類

注意: 
此處一定要通過@Transactional註解來明確指明所用的事務管理器,否則當多數據源時可能只能執行一個數據裏的增刪改操作,且此處應該設置Spring的事務傳播屬性爲propagation =Propagation.NEVER,不使用單個數據源自己的事務,統一交由jtaTransactionManager的事務來管理。

定義GoodsService接口

package com.yyg.boot.service;

import com.yyg.boot.entity.db1.Goods;

import java.util.List;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/7
 * @Description Description
 */
public interface GoodsService {

    List<Goods> findAll();

    Goods addGoods(Goods goods);

}

GoodsServiceImpl實現

package com.yyg.boot.service.impl;

import com.yyg.boot.dao.db01.GoodsRepository;
import com.yyg.boot.entity.db1.Goods;
import com.yyg.boot.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.List;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/7
 * @Description Description
 *
 * 注意:此處一定要通過@Transactional註解來明確指明所用的事務管理器,否則當多數據源時可能只能執行一個數據裏的增刪改操作.
 * 且此處應該設置Spring的事務傳播屬性爲propagation =Propagation.NEVER,不使用單個數據源自己的事務,統一交由jtaTransactionManager的事務來管理.
 */
@Service
@Transactional(transactionManager = "goodsTransactionManager",rollbackFor = Exception.class,propagation =Propagation.NEVER)
public class GoodsServiceImpl implements GoodsService {

    @Autowired
    private GoodsRepository goodsRepository;

    @Override
    public List<Goods> findAll() {
        return goodsRepository.findAll();
    }

    @Override
    public Goods addGoods(Goods goods) {
        try {
            return goodsRepository.save(goods);
        }catch (Exception e){
            //強制手動事務回滾
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return null;
    }

}

定義UserService接口

package com.yyg.boot.service;

import com.yyg.boot.entity.db2.User;

import java.util.List;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/7
 * @Description Description
 */
public interface UserService {

    List<User> findAll();

    User addUser(User user);

}

UserServiceImpl實現

package com.yyg.boot.service.impl;

import com.yyg.boot.dao.db02.UserRepository;
import com.yyg.boot.entity.db2.User;
import com.yyg.boot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.List;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/7
 * @Description Description
 * 注意:此處一定要通過@Transactional註解來明確指明所用的事務管理器,否則當多數據源時可能只能執行一個數據裏的增刪改操作.
 * 且此處應該設置Spring的事務傳播屬性爲propagation =Propagation.NEVER,不使用單個數據源自己的事務,統一交由jtaTransactionManager的事務來管理.
 */
@Service
@Transactional(transactionManager = "userTransactionManager",rollbackFor = Exception.class,propagation =Propagation.NEVER)
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public List<User> findAll() {
        return userRepository.findAll();
    }

    @Override
    public User addUser(User user) {
        try {
            return userRepository.save(user);
        }catch (Exception e){
            //強制手動事務回滾
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return null;
    }

}

8. 創建Controller進行測試

package com.yyg.boot.web;

import com.yyg.boot.entity.db1.Goods;
import com.yyg.boot.entity.db2.User;
import com.yyg.boot.service.GoodsService;
import com.yyg.boot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/3
 * @Description Description
 */
@RestController
public class TransactionController {

    @Autowired
    private UserService userService;

    @Autowired
    private GoodsService goodsService;

    @Transactional(value = "jtaTransactionManager", rollbackFor = Exception.class)
    @GetMapping("/add")
    public ResponseEntity addTest() {
        Goods goods = new Goods();
        goods.setId(44L);
        goods.setName("iPhone se");
        goodsService.addGoods(goods);

        User user = new User();
        user.setSex("男");
        user.setUsername("一一哥");
        user.setAddress("上海");
        user.setBirthday(new Date());
        userService.addUser(user);

        return ResponseEntity.ok("添加成功");
    }

//在該接口中故意製造一個除零異常
@Transactional(value = "jtaTransactionManager", rollbackFor = Exception.class)
    @GetMapping("/add2")
    public ResponseEntity addTest2() {
        Goods goods = new Goods();
        goods.setId(44L);
        goods.setName("iPhone se");
        goodsService.addGoods(goods);

        User user = new User();
        user.setSex("男");
        user.setUsername("一一哥");
        user.setAddress("上海");
        user.setBirthday(new Date());
        userService.addUser(user);

        int i=10/0;

        return ResponseEntity.ok("添加成功");
    }

}

9. 創建入口類

package com.yyg.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @Author 一一哥Sun
 * @Date Created in 2020/4/18
 * @Description Description
 * (exclude = {DataSourceAutoConfiguration.class,DataSourceTransactionManagerAutoConfiguration.class})
 */
@SpringBootApplication
//手動進行事務管理
@EnableTransactionManagement
public class TransactionApplication {

    public static void main(String[] args){
        SpringApplication.run(TransactionApplication.class,args);
    }

}

10. 完整代碼結構


11. 啓動項目進行測試

測試add接口

可以看到,當插入數據沒有異常發生時,兩個數據庫都可以正常插入數據。

測試add2接口

因爲我在add2接口中故意製造了一個除零異常,此時代碼會發生異常。

我們再看數據庫中,會發現並沒有產生新數據,還是和之前的數據一樣,說明多數據源環境下,利用jta-atomikos實現了分佈式事務的代碼實現。




 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章