Spring 数据和 mongodb - 在 @Transactional 中使用 spring 进行简单回滚

Posted

技术标签:

【中文标题】Spring 数据和 mongodb - 在 @Transactional 中使用 spring 进行简单回滚【英文标题】:Spring data and mongodb - simple roll back with spring within @Transactional 【发布时间】:2014-02-18 14:54:58 【问题描述】:

我有 2 个存储库,一个用于 mongodb (DocumentRepository),另一个用于休眠实体 (EntityRepository)

我有一个简单的服务:

 @Transactional
 public doSomePersisting() 
     try 
           this.entityRepository.save(entity);
           this.documentRepository.save(document);
     
     catch(...) 
         //Rollback mongoDB here
     
 

是否可以在“//Rollback mongoDB here”行上回滚mongoDB? 我已经从实体部分得到回滚(事务注释)

【问题讨论】:

【参考方案1】:

MongoDB 不支持事务(至少不超出单个文档的范围)。如果您想回滚更改,则需要自己手工制作。如果您在某些情况下确实需要它们,有一些资源描述了在 Mongo 中实现您自己的事务的方法。你可以看看..

http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/

这只是对您可以使用的模式的解释。如果您发现您的应用程序中绝对需要事务,您应该考虑 MongoDB 是否适合您的需求。

【讨论】:

您可以将 mongo 操作留到最后(就像您在示例中所做的那样)。因此,当它失败时,它将回滚之前的弹簧。您通常需要 mongo 的原子操作,因此这将使您的两个数据源保持一致。除非您使用嵌套事务、分布式事务等,否则这些都不起作用,您必须通过后续的 mongo 更新来弥补。 迟到 ;-) 2018 年 2 月,MongoDB 4.0 发布。它确实支持 ACID 事务。从 Spring Data 2.1 (Lovelace) 开始,您可以将它与 @Transactional 注释一起使用。所以现在应该可以执行真正的两阶段提交了。您还可以看看更简单的解决方案-ChainedTransactionManager,其中可以将Mongodb TransactionManager和关系数据库TransactionManager结合在一起【参考方案2】:

很抱歉重新发布我的答案。

早期的代码允许向 MongoDB 中插入数据,甚至在向 PostgreSQL 中插入数据时抛出查询异常(使用 myBatis)。

我已经解决了 MongoDB 和关系数据库之间的数据事务问题,@Transactional 通过在上述代码中进行这些更改完美地工作。

@Transactional 管理的解决方案。

Mongo 配置类

@Configuration
public class MongoConfig extends AbstractMongoConfiguration
    private static final Logger LOG = LoggerFactory.getLogger(MongoConfig.class);

    @Value("$spring.data.mongodb.database")
    private String dbName;

    @Value("$spring.data.mongodb.host")
    private String dbHost;

    @Value("$spring.data.mongodb.port")
    private int dbPort;

    @Override
    public String getDatabaseName() 
        return dbName;
    

    @Bean
    public MongoClient mongoClient()
        return new MongoClient(dbHost, dbPort);
    

    @Bean
    public MongoDbFactory mongoDbFactory()
        return new SimpleMongoDbFactory(mongoClient(),dbName);
    

    @Bean
    public MongoTemplate mongoTemplate() 
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
        MappingMongoConverter mappingMongoConverter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
        // Don't save _class to mongo
        mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory(),mappingMongoConverter);
        mongoTemplate.setSessionSynchronization(SessionSynchronization.ON_ACTUAL_TRANSACTION);
        return mongoTemplate;
    

    public MongoTemplate fetchMongoTemplate(int projectId) 
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
        MappingMongoConverter mappingMongoConverter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
        // Don't save _class to mongo
        mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        MongoDbFactory customizedDBFactory = new SimpleMongoDbFactory(mongoClient(), dbName+"_"+projectId);
        MongoTemplate mongoTemplate = new MongoTemplate(customizedDBFactory,mappingMongoConverter);
        MongoTransactionManager mongoTransactionManager = new MongoTransactionManager(customizedDBFactory);
        return mongoTemplate;
    

    @Bean
    public MongoTransactionManager mongoTransactionManager() 
        return new MongoTransactionManager(mongoDbFactory());
    


数据插入服务类

@Service
@Component
public class TestRepositoryImpl implements TestRepository
    private static final Logger LOG = LoggerFactory.getLogger(TestRepositoryImpl.class);


@Autowired MongoConfig mongoConfig;
@Autowired MongoTemplate mongoTemplate;
@Autowired MongoTransactionManager mongoTransactionManager;

@Autowired UserService userService;

@Override
@Transactional
public void save(Test test)
    int projectId = 100;
    if (projectId != 0) 
        mongoTemplate = mongoConfig.fetchMongoTemplate(100);
        mongoTemplate.setSessionSynchronization(SessionSynchronization.ALWAYS);
    
    mongoTemplate.insert(test);
    IdName idName = new IdName();
    idName.setName("test");
    mongoTemplate.insert(idName);
    User user = new User();
    user.setName("Demo");
    user.setEmail("srini@abspl.in");
    user.setPassword("sdfsdfsdf");
    userService.save(user);
    
 

POM.XML

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.abcplusd.sample.mongoapi</groupId>
  <artifactId>sample-mongo-api</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>Sample Spring Boot Mongo API</name>
  <description>Demo project for Spring Boot Mongo with Spring Data Mongo</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-mongodb</artifactId>
      <version>2.1.0.RELEASE</version>
      <exclusions>
        <exclusion>
          <groupId>org.mongodb</groupId>
          <artifactId>mongo-java-driver</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-commons</artifactId>
      <version>2.1.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.mongodb</groupId>
      <artifactId>mongo-java-driver</artifactId>
      <version>3.8.2</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <version>42.2.2</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.2</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.5</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

【讨论】:

嘿,你能分享一下你的项目设置(build.gradle 或 pom.xml)吗?我很难在 spring 上设置这个 @Ben 请在上面的答案中找到 pom.xml。 @Srini 什么表示 projectId ?我每次都需要传递什么价值 @pudaykiran ProjectId 用于在 mongodb 中创建动态数据库名称。请参考上述代码 sn-p 中的 MonoConfig 类。【参考方案3】:

使用 MongoDb 4.0.x,您可以使用事务。如果您使用以下版本,则必须实现两阶段提交。

注意:MongoDb 仅允许您在拥有 ReplicaSet 时使用事务。

要同时为 JPA 和 MongoDb 使用事务,您必须使用 ChainedTransactionManager。过程是:

创建 Jpa 事务管理器 创建 MongoDb 事务管理器 创建将使用上述两个的 ChainedTransactionManager

我的 conf 看起来像这样(我不使用 spring boot,但它应该是等效的):

Jpa 配置

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories("com....")
public class HibernateConfig 

    //define entity manager, data source and all the stuff needed for your DB

    @Bean("jpaTransactionManager")
    public JpaTransactionManager transactionManager() throws NamingException  

        JpaTransactionManager transactionManager = new JpaTransactionManager();
        //transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());

        return transactionManager;
    

MongoDb 配置

@Configuration
@EnableMongoRepositories(basePackages = "com....")
public class MongoDbConf extends AbstractMongoClientConfiguration 

    private final Environment environment;

    @Autowired
    public MongoDbConf(Environment environment) 
        this.environment = environment;
    

    @Override
    public MongoClient mongoClient() 
        String connectionString = environment.getProperty("mongodb.connectionString");

        if(StringUtils.isBlank(connectionString))
            throw new IllegalArgumentException("No connection string to initialize mongo client");

        return MongoClients.create(
                MongoClientSettings.builder()
                        .applyConnectionString(new ConnectionString(connectionString))
                        .applicationName("MY_APP")
                        .build());
    

    @Override
    protected String getDatabaseName() 
        return environment.getProperty("mongodb.database", "myDB");
    

    @Bean("mongoDbTransactionManager")
    public MongoTransactionManager transactionManager(MongoDbFactory dbFactory) 
        return new MongoTransactionManager(dbFactory);
    

ChainedTransactionManager 配置

@Configuration
public class ChainedTransactionConf 

    private MongoTransactionManager mongoTransactionManager;
    private JpaTransactionManager jpaTransactionManager;

    @Autowired
    public ChainedTransactionConf(MongoTransactionManager mongoTransactionManager, JpaTransactionManager jpaTransactionManager) 
        this.mongoTransactionManager = mongoTransactionManager;
        this.jpaTransactionManager = jpaTransactionManager;
    

    @Bean("chainedTransactionManager")
    public PlatformTransactionManager getTransactionManager() 
        ChainedTransactionManager transactionManager = new ChainedTransactionManager(jpaTransactionManager, mongoTransactionManager);
        return transactionManager;
    


mongoDb 存储库示例

@Service
public class MongoDbRepositoryImpl implements MongoDbRepository 

    private static final Logger logger = Logger.getLogger(MongoDbRepositoryImpl.class);

    //MongoOperations will handle a mongo session
    private final MongoOperations operations;

    @Autowired
    public MongoDbRepositoryImpl(MongoOperations operations) 
        this.operations = operations;
    

    @Override
    public void insertData(Document document) 
        MongoCollection<Document> collection = operations.getCollection("myCollection");
        collection.insertOne(document);
    

在您的服务中使用事务

@Service
public class DocumentServiceImpl implements DocumentService 

    private final MongoDbRepository mongoDbRepository;
    private final JpaRepository jpaRepository;

    @Autowired
    public DocumentServiceImpl(MongoDbRepository mongoDbRepository,JpaRepository jpaRepository) 
        this.mongoDbRepository = mongoDbRepository;
        this.jpaRepository = jpaRepository;
    

    @Override
    @Transactional("chainedTransactionManager")
    public void insertNewDoc(Map<String,Object> rawData) 
        //use org.springframework.transaction.annotation.Transactional so you can define used transactionManager
        //jpaRepository.insert...
        Document mongoDoc = new Document(rawData);
        mongoDbRepository.insertData(mongoDoc)

        //you can test like this : breakpoint and throw new IllegalStateException() 
        //to see that data is not commited 
    

【讨论】:

【参考方案4】:

MongoDB v4.x.x 与 @Transactional 完美配合,它们通过使用以下依赖项和存储库明确支持这一点:-

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-releasetrain</artifactId>
    <version>Lovelace-M3</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

还有一个 MongoTransactionConfig 类:-

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;

@Configuration
@EnableMongoRepositories(basePackages = "com.airtel.africa.caxr.repository")
public class MongoTransactionConfig extends AbstractMongoClientConfiguration 

    @Value("$spring.data.mongodb.host")
    private String host;
    @Value("$spring.data.mongodb.port")
    private String port;
    @Value("$spring.data.mongodb.database")
    private String database;

    @Bean
    MongoTransactionManager transactionManager(MongoDbFactory dbFactory) 
        return new MongoTransactionManager(dbFactory);
    

    @Override
    protected String getDatabaseName() 
        return database;
    
    @Override
    public MongoClient mongoClient() 
        String connectionString = "mongodb://"+host+":"+port;

        return MongoClients.create(MongoClientSettings.builder()
            .applyConnectionString(new 
        ConnectionString(connectionString)).build());
    

在这里,我将 mongo 与 kafka 一起用作 1 事务,因此如果此处发生任何已检查或未检查的异常,则应回滚 mongo 事务,因此我使用了 @Transactional(rollbackFor = Exception.class):-

@Transactional(rollbackFor = Exception.class)
public void receiveInEventRequest(TransactionDto transactionDto) throws 
    InterruptedException, ExecutionException 
    // db insert    
    TransactionRequest transactionRequest = requestDbDumpService.dumpToDb(transactionDto);
    // kafka insert
    ListenableFuture<SendResult<String, TransactionDto>> kafkaResult = kafkaTemplate.send(kafkaProducerQueueName, “ID”, transactionDto);
    SendResult<String, TransactionDto> kafkaSendResult = kafkaResult.get();

【讨论】:

【参考方案5】:

如果有人需要 transactional 支持 reactive 风格 spring bootMongoDb 集成,请查看答案 @987654321 @

【讨论】:

以上是关于Spring 数据和 mongodb - 在 @Transactional 中使用 spring 进行简单回滚的主要内容,如果未能解决你的问题,请参考以下文章

Spring 数据和 mongodb - 在 @Transactional 中使用 spring 进行简单回滚

使用Spring启动的DB2和MongoDB的Spring批处理

如何在带有自定义过滤器的 Spring Data mongodb 中使用分页和排序?

mongoDB 插入数据

如何使`org.mongodb.driver.cluster`在spring boot中使用嵌入式mongodb?

使用 springdoc-openapi 和 spring-boot-starter-data-mongodb 生成 OpenAPI 文档