MongoDB的事务,可以实现和关系型数据库类似的事务场景
必须使用与 MongoDB 4.2 兼容的驱动
事务默认必须在 60s(可调)内完成,否则将被取消
涉及事务的分片不能使用仲裁节点
事务会影响 chunk 迁移效率。正在迁移的 chunk 也可能造成事务提交失败,此时重试即可
多文档事务中的读操作必须使用主节点读
readConcern 只应该在事务级别设置,不能设置在每次读写操作上

原子性(A)

事务的原子性是指事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,要么全部执行,要么全部不执行。

任何一项操作失败都将导致整个事务失败,同时其他已经被执行的操作都将被撤销并回滚。只有所有的操作全部成功,整个事务才算是成功完成。

一致性(C)

事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行前后,数据库都必须处于一致性状态。换句话说,事务的执行结果必须是使数据库从一个一致性状态转变到另一个一致性状态。

银行的转账操作就是一个事务。假设 A 和 B 原来账户都有 100 元。此时 A 转账给 B 50 元,转账结束后,应该是 A账户减去 50 元变成 50 元,B 账户增加 50 元变成 150 元。A、B 的账户总和还是 200 元。转账前后,数据库就是从一个一致性状态(A 100 元,B 100 元,A、B 共 200 元)转变到另一个一致性状态(A 50 元,B 150 元,A、B共 200元)。假设转账结束后只扣了 A 账户,没有增加 B 账户,这时数据库就处于不一致的状态。

隔离性(I)

事务的隔离性是指在并发环境中,并发的事务是相互隔离的,事务之间互不干扰。

在标准的SQL规范中,定义的 4 个事务隔离级别,不同隔离级别对事务的处理不同。4个隔离级别分别是:未授权读取、授权读取、可重复读取、串行化。

脏读

当前事务A 中可以读到其他事务B 未提交的数据(脏数据),这种现象是脏读。

不可重复读

**
在事务A 中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据

幻读

**
在事务A 中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了

下表展示了不同隔离级别下事务访问数据的差异

隔离级别 脏读 可重复读 幻读
读未提交 存在 不可以 存在
读已提交 不存在 不可以 存在
可重复读取 不存在 可以 存在
串行化 不存在 可以 不存在

以上 4 个级别的隔离性依次增强,分别解决不同的问题。事务隔离级别越高,就越能保证数据的完整性和一致性,但同时对并发性能的影响也越大

通常,对于绝大多数的应用来说,可以优先考虑将数据库系统的隔离级别设置为
授权读取**,这能够在避免脏读的同时保证较好的并发性能。尽管这种事务隔离级别会导致不可重复读、幻读和第二类丢失更新等并发问题,但较为科学的做法是在可能出现这类问题的个别场合中,由应用程序主动采用悲观锁或乐观锁来进行事务控制。

持久性(D)

事务的持久性又称为永久性,是指一个事务一旦提交,对数据库中对应数据的状态变更就应该是永久性的。即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态。

写

writeConcern决定一个写操作落到多少个节点上才算成功。writeConcern的取值包括:

  • 0:发起写操作,不关心是否成功
  • 1~集群最大数据节点数:写操作需要被复制到节点执定节点数才算成功
  • majority: 写操作需要被复制到大多数节点才算成功。

发起写操作的程序将阻塞到写操作到达指定的节点数为止
journal 则定义如何才算成功。

  • true:写操作落到 journal 文件中才算成功;
  • false:写操作到达内存即算做成功。

使用:当指定了超过节点数量时,会提示报错,但是数据还是会写入成功

  1. db.test.insert({count:2}, {writeConcern:{w:3}})

writeConcern 实验

查看节点设置

  1. var conf=rs.conf()
  2. conf.members

设置延迟节点

  1. conf.members[1].slaveDelay=10 // 设置延迟
  2. conf.members[1].priority=0 // 设置不参与选举
  3. rs.reconfig(conf) // 使配置生效
  4. 命令执行后,会强制当前的主节点下线,然后进行新的主节点选择。
  5. 主节点下线时,会关闭所有客户端的连接,这个过程会持续10-20s,
  6. 因此该操作应该在维护时间执行,减少对系统的影响。

设置超时时间

  1. db.tx.insert({count:10}, {writeConcern:{w:3}})
  2. db.tx.insert({count:10}, {writeConcern:{w:3, wtimeout:3000}})

读

  • readPreference:决定读哪个节点
  • primary:只选择主节点(默认)
  • primaryPreferred:优先选择主节点,如果不可用则选择从节点
  • secondary: 只选择从节点
  • secondaryPreferred:优先选择从节点,如果从节点不可用则选择主节点
  • nearest:选择最近的节点;

readPreference 选择从节点读实验

  1. db.test.remove({})
  2. db.test.insert({count:100})
  3. 在两个从节点分别执行
  4. db.fsyncLock() // 从节点执行 禁止同步(写入)
  5. db.test.insert({count:300}) // 主节点 插入一条数据
  6. db.test.find().readPref("secondary").readConcern("majority") // 主节点 查找
  7. db.fsyncUnlock() // 从节点执行 解锁

什么是 readConcern?

在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据那些是可读的,类似关系型数据库的隔离级别。可选值包括:

  • available:读取所有可用的数据
  • local:读取所有可用且属于当前分片的数据
  • majority:读取在大多数节点上提交完成的数据
  • linearizable:可线性化读取文档
  • snapshot:读取最近快照中的数据

实验

使用snapshot 可重读,幻读,改实验证明不需要

  1. db.tx.remove({})
  2. db.tx.insert({x:1})
  3. var session=db.getMongo().startSession();
  4. session.startTransaction({readConcern:{level:"snapshot"},writeConcern:{w:"majority"});
  5. var coll=session.getDatabase('taibai').getCollection("tx");
  6. coll.findOne({x:1})
  7. db.tx.updateOne({x:1},{$set:{y:1}})
  8. db.tx.find({x:1})
  9. coll.findOne({x:1})
  10. session.abortTransaction()
  11. session.commitTransaction();

实现安全的读写分离

**
写使用:writeConcern majority
读使用:readConcern majority

MongoDB ACID多文档事务支持


事务属性 支持程度
A 原子性 单表单文档 : 1.x 就支持复制集多表多行:4.0复制集 分片集群多表多行4.2
C 一致性 writeConcern,readConcern(3.2)
I 隔离性 readConcern (3.2)
D 持久性 Journal and replication