不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

Springboot教程系列(4) Springboot中的MonogoDB多文档事务

Springboot中的MonogoDB多文档事务

前言

NOSQL事务之殇

说到关系数据库的事务,开发同学们一定耳熟能详,原子性、隔离性、一致性、持久性是不是如数家珍?熟悉Spring框架的还深谙在Spring中如何优雅的控制事务。但是我们知道Spring本身没有实现事务,其事务控制也是以来底层关系型数据库的。

对使用MongoDB的同学来说,当然不能使用了。因为在MongoDB里单文档的操作是具备原子性的,多文档就不支持了,所在设计MongoDB数据库的时候,就会用到嵌入和引用来规避事务控制,如果向银行转账之类的操作,无法完全规避的情况下,也是通过自己创建一个容器文档,来做仿真事务的。但是在MongoDB4.0中,支持了我们梦寐以求的多文档事务。

官方消息解读

来看下官方文档中的介绍:

Best way to work with data: adding multi-document ACID transactions, data type conversions, native visualizations with MongoDB Charts, the MongoDB Compass aggregation pipeline builder, and the MongoDB Stitch serverless platform.

Intelligently place data where you need it to be: with 40% faster shard migrations, non-blocking secondary replica reads, SHA-2 authentication, and the new MongoDB Mobile database.

Freedom to run anywhere: bringing global clusters and enterprise security with HIPAA compliance to the MongoDB Atlas database service, along with the free community monitoring service, and Kubernetes integration.

我们可以看到4.0中优化了不少特性,但今天只要关注第一条的开头一句就够了: adding multi-document ACID transactions(增加了多文档ACID事务),接下来我们可以喜大普奔了。

MongoDB新版本提供了面向复制集的多文档事务特性。其能满足在多个操作,文档,集合,数据库之间的事务性,事务的特性:一个事务中的若干个操作要么全部完成,要么全部回滚,操作的原子性A,数据更新的一致性C。事务提交时,所有数据更改都会被永久保存D。事务提交前其数据更改不会被外部获取到。还有个重磅消息:多文档事务在4.0版本仅支持复制(副本)集,对分片集群的事务性支持计划在4.2版本中实现。

不过有个前提,事务的ACID操作必须是针对一个已经存在的集合进行,详细的方法和介绍大家可以去读读官方的文档。我接下来要介绍的是我们如何在Springboot中实现MongoDB的多文档事务操作。

阅读下面的内容需要你具备以下知识:对Springboot有一定的使用经验,了解MongoDB的基本概念掌握基本使用。

环境准备

MongoDB环境准备

  • 版本检查:版本必须是4.0+的,如果不是新安装而是老版本升级的,要检查下自己数据库featureCompatibilityVersion参数是不是4.0
db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

如果不是的话设置下:db.adminCommand( { setFeatureCompatibilityVersion: "4.0" } )

  • 副本集群搭建:因为多文档事务前提就是需要多个副本集,所以我们至少准备2个副本集,我这里准备的是在内网环境中的一台机器上采用不同的端口实现多副本集,生产环境下一般都会分开。

主副本:192.168.3.112:27117,辅助副本:192.168.3.112:27217

其中一个的配置文件如下:

port = 27117  端口,另一个是27217
dbpath =../dbs/   存放数据文件,相同机器时要不同
logpath=../logs/mongdb.log  存放日志文件, 相同机器时要不同
logappend=true
fork=true
bind_ip=0.0.0.0
replSet = myrepl   集群名称,自己定义

副本集初始化命令:

rs.initiate({_id:"myrepl",members:[
{_id:0,host:'192.168.3.112:27117'},
{_id:1,host:'192.168.3.112:27217'}]
})

注意:这里的IP不是本机的话,一定要填写内网具体的地址,不能用回写地址127.0.0.1;因为这个IP是要和客户端通信的。

配置成功后可以使用rs.config() 来查看下,如果初始化时候信息填错,可以使用rs.reconfig({...})来重新设置下

{
    "_id" : "myrepl",
    "version" : 2,
    "protocolVersion" : NumberLong(1),
    "writeConcernMajorityJournalDefault" : true,
    "members" : [ 
        {
            "_id" : 0,
            "host" : "192.168.3.112:27117",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1.0,
            "tags" : {},
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        }, 
        {
            "_id" : 1,
            "host" : "192.168.3.112:27217",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1.0,
            "tags" : {},
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        }
    ],
    "settings" : {
        "chainingAllowed" : true,
        "heartbeatIntervalMillis" : 2000,
        "heartbeatTimeoutSecs" : 10,
        "electionTimeoutMillis" : 10000,
        "catchUpTimeoutMillis" : -1,
        "catchUpTakeoverDelayMillis" : 30000,
        "getLastErrorModes" : {},
        "getLastErrorDefaults" : {
            "w" : 1,
            "wtimeout" : 0
        },
        "replicaSetId" : ObjectId("5b6aca1b7ef20d701c739318")
    }
}

Springboot环境准备

  • 加入依赖:本人比较喜欢使用Gradle来管理包依赖,喜欢Maven的同学可以自己根据其语法配置。
    需要注意的是依赖包的版本是要有严格限制的,因为我们目前使用的是spring-data-mongodb的RC版本,目前稳定版中还不支持,所以drive包必须是3.8的,common包里必须是对应的RC版,我们要把自动依赖的低版本包去除掉。依赖包如下所示:
dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-mongodb'){
        exclude group: 'org.springframework.data', module: 'spring-data-mongodb'
        exclude group: 'org.mongodb', module: 'mongodb-driver'
    }
    compile ('org.springframework.data:spring-data-mongodb:2.1.0.RC1'){
        exclude group: 'org.springframework.data', module: 'spring-data-commons'
    }
    compile ('org.mongodb:mongo-java-driver:3.8.0')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.data:spring-data-commons:2.1.0.RC1')

数据库查看根据

如果还用客户端直接访问服务端来查看MongoDB集合或文档的话,那效率也太低了,工欲善其事必先利其器嘛。所以免费的工具还是推荐Robo 3T,当然如果你使用的是收费的Studio 3T那真的最好了。

其操作界面如下:

与Springboot结合

用Spring注解方式来控制事务

要使用Spring注解方式控制事务,需要使用使用到MongoDB提供的事务管理类,我们要先配置一个

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

编写测试类代码

然后创建好需要测试的控制类,服务类,资源类。篇幅原因就不把各个部分的代码贴上来了,只简单介绍下注意点,源代码请到我的个人仓库查看:https://gitee.com/hzqiuxm/mddemo.git
文档实体类Person.java

@Document
public class Person {
    @Id
    private ObjectId id;
    private String name;
    private int age;
    private List<String> address;
    private Map<String,String> des;
     ......
   }

TestController.java是控制类,里面的方法testAdd()用来测试添加一个文档

@GetMapping("/testAdd")
public void testAdd(){

    Person person = new Person("fengxiaoxiong",29);

    ArrayList<String> strings = new ArrayList<>();
    strings.add("浙江省杭州市西湖区");
    strings.add("浙江省杭州市之江区");
    person.setAddress(strings);

    Map<String,String> descrption = new HashMap<>();
    descrption.put("ext1","my");
    descrption.put("ext2","bear");

    person.setDes(descrption);

    personService.addPerson(person);
}

PersonService 是服务类,自动注入实体操作类,并增加一个addPerson()方法,用来添加一个文档。

@Autowired
private PersonRepository personRepository;
@Transactional
public void addPerson(Person person){
    personRepository.insert(person);
}

我们使用注解@Transctional来控制事务
最后的PersonRespository接口是用来操作数据集集合文档的,继承自MongoDBRepository接口,简单的增删改查方法框架都帮我们生成好了,所以不用增加任何方法。

public interface PersonRepository extends MongoRepository<Person,ObjectId> {
}

创建数据库集合和配置文件

本次用到的数据库名称:sale;集合名称Person
在Springboot工程里application.yml配置文件中加入:

spring:
    data:
      mongodb:
        host: 192.168.3.112  //主副本的地址
        port: 27117          //主副本的端口
        replica-set: myrepl  //集群名称
        database: sale       //数据库名称

注意,这里只要配置主副本的地址和端口就可以了,工程启动的时候我们可以从启动日志看到通过连接主副本后,会解析集群的信息然后分别连接到各个副本

测试

无事务方法中出现异常情况

在服务方法中我们加入一个除0的异常代码

public void addPerson(Person person){
    personRepository.insert(person);
    int a = 5/0;
}

这种情况下,因为没有事务文档是会添加成功的,执行后报java.lang.ArithmeticException: / by zero异常错误,但是我们查看数据库,发现记录被添加了:

/* 6 */
{
    "_id" : ObjectId("5b6bb1a29391ca5a04d13514"),
    "name" : "fengxiaoxiong",
    "age" : 29,
    "address" : [ 
        "浙江省杭州市西湖区", 
        "浙江省杭州市之江区"
    ],
    "des" : {
        "ext2" : "bear",
        "ext1" : "my"
    },
    "_class" : "com.example.md.mddemo.Person"
}

有事务方法中出现异常情况

因为加了事务,所以会避免提交,我们把要把TestController.java中testAdd()代码中插入内容修改下:

Person person = new Person("jinguanghui",31);
ArrayList<String> strings = new ArrayList<>();
strings.add("浙江省杭州市余杭区");
strings.add("浙江省杭州市之江区");
person.setAddress(strings);
Map<String,String> descrption = new HashMap<>();
descrption.put("ext1","so");
descrption.put("ext2","cool");

然后把事务注解加回到服务方法addPerson()上

@Transactional
public void addPerson(Person person){
    personRepository.insert(person);
    int a = 5/0;
}

执行后报java.lang.ArithmeticException: / by zero异常错误,但是我们查看数据库,发现记录未被添加事务控制起作用了

有事务无异常情况

最后我们把产生异常的代码 a=5/0删除掉执行,文档被正常插入了

/* 7 */
{
    "_id" : ObjectId("5b6bb3c49391ca5a788cfcfa"),
    "name" : "jinguanghui",
    "age" : 31,
    "address" : [ 
        "浙江省杭州市余杭区", 
        "浙江省杭州市之江区"
    ],
    "des" : {
        "ext2" : "cool",
        "ext1" : "so"
    },
    "_class" : "com.example.md.mddemo.Person"
}

总结

虽然上面只简单使用了一个除0异常来演示,但是原理上是相同的,我们可以在除0异常后加入其它文档操作,最后效果类似。

通过这个简单示例,我们现在如果要使用MongoDB的多文档事务控制功能,必须要注意下面几点:

  • MongoDB的版本必须是4.0
  • 事务功能必须是在多副本集的情况下才能使用
  • 如果不和其他框架结合使用,直接参考官方文档的显示调用方式
  • 事务控制只能用在已存在的集合中
  • 如果想和Springboot框架结合,借用spring-data-mongodb来简化操作,那目前只能使用RC包,并且要剔除老版本包的依赖,自己添加符合要求的包

相信本次事务功能的更新,对MongoDB数据库模式设计会带来一些变化,嵌入文档,数组结合引用和仿真事务的一些设计会有所变化,一些棘手的问题会在一定程度上得到解决。

最后我们期待分片集群事务早点到来,广大Springboot的使用者们相信更期待spring-data-mongodb稳定版对事务支持早日推出。

未经允许不得转载:菡萏如佳人 » Springboot教程系列(4)

欢迎加入极客江湖

进入江湖关于作者