MongoDB 事务与数据落盘

概要

MongoDB并不像MySQL一样天然支持多文档事务,其演变过程如下:

  • MongoDB4.0之前只支持单文档事务,在单个文档上支持ACID原子性,并且不对外暴漏API,用户无法控制事务,完全是MongoDB自行控制;
  • MongoDB4.0开始支持多文档事务以及复制集和分片集群下的事务,统称为分布式事务,并提供API允许用户像MySQL事务那样控制事务的开始与结束。

但是MongoDB4.0的事务仍有限制:

  1. 事务的默认最大运行时间是 60s。
    1)通过在 mongod 实例级别上修改transactionLifetimeLimitSeconds 的限制来增加。对于分片集群,必须在所有分片副本集成员上设置该参数。超过此时间后,事务将被视为已过期,并由定期运行的清理进程中止。清理进程每 60 秒或每 transactionLifetimeLimitSeconds/2 运行一次,以较小的值为准。
    2)要显式设置事务的时间限制,建议在提交事务时指定 maxTimeMS 参数。如果 maxTimeMS 没有设置,那么将使用 transactionLifetimeLimitSeconds;如果设置了 maxTimeMS,但这个值超过了 transactionLifetimeLimitSeconds,那么还是会使用 transactionLifetimeLimitSeconds。
    3)事务等待获取其操作所需锁的默认最大时间是 5 毫秒。可以通过修改由maxTransactionLockRequestTimeoutMillis 参数控制的限制来增加。如果事务在此期间无法获得锁,则该事务会被中止。maxTransactionLockRequestTimeoutMillis 可以设置为 0、-1 或大于 0 的数字。将其设置为 0 意味着,如果事务无法立即获得所需的所有锁,则该事务会被中止。设置为 -1 将使用由 maxTimeMS 参数所指定的特定于操作的超时时间。任何大于 0 的数字都将等待时间配置为该时间(以秒为单位)以作为事务尝试获取所需锁的指定时间段。

  2. MongoDB 将当前事务中的所有写操作日志放入单文档中,而oplog只是一个特殊的collection,其单个文档也象正常集合一样受16MB的大小限制。这个限制在MongoDB4.2做了很大的优化,即调整成为当前事务中的每一个写操作日志都创建一个单文档。

综上两点限制,不管如何还是要避免长事务与大事务。

MongoDB的oplog相当于MySQL的binlog,用于主备节点数据复制
MongoDB的journal log相当于MySQL的redo log,用于实现事务持久性,尽量保证数据不丢失以及崩溃恢复

事务的四大特性这里就不多说了,这里主要聊一下MongoDB的事务持久性与隔离性。

PS:针对MongoDB的WiredTiger存储引擎

一、持久性

持久性是指一个事务提交后,其所做的写操作会被永久保存到数据库中,即使此时数据库or操作系统崩溃,修改的数据也不会丢失。
下面我们来看看MongoDB事务持久性是否是这样的,先看一下下图:

MongoDB数据落盘示意图
我们已经知道journal log记录的是最新的写操作内容,即redo,所以只要写的内容到了journal log,MongoDB就可以在崩溃重启后恢复。

1.1、MongoDB数据文件

mongodb数据文件

  1. collection-xxx.wt和index-xxx.wt文件
    这是数据库中集合所对应的数据文件和索引文件。
  2. WiredTiger.lock文件
    这是WiredTiger运行实例的锁文件,防止多个进程同时连接同一个Wiredtiger实例。

如MongoDB启动后,默认被当作一个应用连接到WiredTiger(表示文件锁已被占用),当想执行其它wt命令时会报如下错误:
wiredtiger_open: __posix_file_lock, 391: ./WiredTiger.lock: handle-lock: fcntl: Resource temporarily unavailable
wiredtiger_open: __conn_single, 1682: WiredTiger database is already being managed by another process: Device or resource busy

  1. mongod.lock文件
    这是MongoDB启动后在磁盘上创建的一个与守护进程mongod相关的锁文件,这个文件会记录mongod在运行过程中的一些状态信息,当正常关闭mongod时,会清除mongod.lock文件里面的内容;如果mongod.lock文件内容没有被清除,则说明mongod非正常的关闭;
  2. storage.bson文件
    这是一个BSON格式的二进制文件,其内容与WiredTiger存储引擎的配置有关,可以通过MongoDB提供的bsondump命令工具查看其内容。

执行如下命令:
./bin/bsondump storage.bson
输出信息如下:
{“storage”:{“engine”:”wiredTiger”,”options”:{“directoryPerDB”:false,”directoryForIndexes”:false,”groupCollections”:false}}}

  1. sizeStorer.wt文件
    存储所有集合的容量信息,如集合中包含的文档数、总数据大小。
    注意:如果MongoDB数据库实例非正常关闭,可能有insert/delete等操作修改的数据并没有持久化,因此集合中的文档记录和元数据文件sizeStorer.wt保存的记录数可能不一致。
  2. _mdb_catalog.wt文件
    存储的是集合表名与磁盘上数据文件和索引文件间的对应关系。这个映射关系也可以通过前面介绍的集合命令:db.account.stats({“indexDetails”:true})获得。
  3. WiredTiger文件
    存储的是WiredTiger存储引擎的版本号,编译时间等信息。
  4. WiredTiger.wt文件
    存储的是所有集合(包含系统自带的集合)相关数据文件和索引文件的checkpoint信息。
  5. WiredTiger.turtle文件
    存储的是WiredTiger.wt这个文件的checkpoint数据信息。相当于对保存有所有集合checkpoints信息的文件WiredTiger.wt又进行了一次checkpoint。
  6. WiredTigerLAS.wt文件
    存储的是内存里面lookaside table的持久化的数据。当对一个page进行reconcile时,如果系统中还有之前的读操作正在访问此page上的修改数据,则会将这些数据保存到lookaside table;当page再被读时,可以利用此lookaside table中的数据重新构建内存page。
  7. diagnostic.data文件夹
    存放的是MongoDB启动运行时的诊断数据。
  8. journal文件夹
    开启Jouranl日志功能后,存放的是Write ahead log事务日志,当数据库意外crash时,可通过log来恢复数据
1.2、journal log刷盘机制

journal log主要受两个配置参数控制。

storage:
   journal:
      enabled: <boolean>  #是否开启journal log
      commitIntervalMs: <num> #journal log刷盘的间隔,默认100ms,范围是1-500ms,值越小,丢失数据越少,性能越低;

除了配置文件,也可以通过如下命令调整journalCommitInterval的值:

db.adminCommand({"setParameter":1,"journalCommitInterval":10}); #设置为10ms

可知最多有journalCommitIntervalms的数据丢失。

另外在db.collection.insert({x:1}, {writeConcern: {j: true}})写命令中可以通过设置 j 的值为true来确保该语句的journal log刷盘。当然这并不意味着每一个写操作就等于一个IO。MongoDB并不会对每一个操作都立即刷盘,而是会等最多30ms,把30ms内的写操作集中到一起,采用顺序追加的方式写入到盘里。在这30ms内客户端线程会处于等待状态。这样对于单个操作的总体响应时间将有所延长,但对于高并发的场景,综合下来平均吞吐能力和响应时间不会有太大的影响。特别是你能给journal部署一个对顺序写有优化的IO带宽足够的专门的存储系统的话,这个对性能的影响可以降到最低。

还有就是缓冲区buffer中的journal log大小达到100MB(因为journal log文件的大小限制是100MB)也会触发刷盘。

总共三种,可以看到在写操作语句中指定j: true可确保journal log刷盘,保证数据不丢失。

1.3、checkpoint机制

MongoDB的数据刷盘也有和MySQL一样的checkpoint机制,触发条件如下:

  • 按一定时间周期(由storage.syncPeriodSecs控制):默认60s,执行一次checkpoint;
  • 按journal log文件大小:当journal log文件大小达到2GB(如果已开启),执行一次checkpoint;

从数据刷盘机制可知,MongoDB的持久性只要靠journal log保证。

1.4、缓存page逐出机制

MongoDB启动时默认占有较大的内存(服务器内存*50%-1G和256MB中的最大值),但是仍有可能被耗尽,原因如下:

  • WiredTiger只有journal log(即redo log),没有像MySQL一样的undo log,就要求文档只有在事务提交后才能被持久化,否则只能待在内存中,所以会消耗很多内存;
  • WiredTiger的事务回滚和快照读依赖MVCC,一个文档增删改的多个版本是存储在内存中的,不会被持久化,不同于MySQL的MVCC基于undo log实现,会落盘,所以也会消耗很多内存。

MongoDB为了避免内存耗尽,有以下三种机制:

  1. cursor在访问btree的page时,当发现page的内存占用量超过了memory_page_max(MongoDB的配置值是10MB),就会对它做逐出操作,以减小page的内存占用量;
  2. 后台eviction线程根据lru queue排序逐出page;
  3. 内存压力大时,用户线程会根据lru queue排序逐出page,这种时候反映在客户端就是MongoDB响应很慢。

详情见本文:wiredtiger page逐出
PS:checkpoint其实也是一种内存逐出机制,但是比较特殊就单独列出去了

1.5、复制集下的写安全机制

官方文档writeConcern说明

db.test.insert({x : 1}, {writeConcern: {w : 1,j : 1,wtimeout : 5000}}) #默认w值为1
  1. {w:0} 即Unacknowledged
    Unacknowledged指的是对每一个写入操作,MongoDB并不会返回一个是否成功的状态值。这个级别是写入性能最好但也是最不安全的级别。比如说,你试图插入一个违反了唯一性的文档(重复的身份证号),那么MongoDB会拒绝写入并报错。但是由于驱动端并没有在乎你的报错,应用程序还满心欢喜以为一切都没问题,下回再来查询那条数据的时候就会出现数据缺失的情况。
  2. {w:1} 即Acknowledged
    Acknowledged 的意思就是对每一个写入MongoDB都会确认操作的完成状态,不管是成功还是失败。当然这个确认只是基于主节点的内存写入。但是这个级别,已经可以侦测到重复主键, 网络错误,系统故障或者是无效数据等错误。
  3. {w:“majority”} 写到多数节点
    MongoDB 的默认部署是至少3个节点的复制集(Replicaset),使用复制集的好处很多,最关键的就是提高系统的高可用性。但是也带来了一个问题,主备不一致,该参数就可以很好的解决该问题。
    假设复制集中有A、B、C三个节点,A为主节点,此时w=1,那么:
    1)在A接收一个写命令x并返回成功时,A与B,C失联了;
    2)下一刻A发现自己无法和从节点B,C 联络上,主动降级为从节点,停止接受写操作;
    3)B、C选举出B为主节点,接收客户端请求,稍后网络恢复A节点重新加入复制集。这个时候A的oplog 和B的oplog已经有不一致了。A会主动把B上面不存在的写操作回滚掉(rollback),并写入一个回滚文件。
    这个时候应用如果再去查询写命令x的内容,MongoDB 将会说文档不存在,w=majority就避免了该问题。

所以说,可以通过w和j的值合理安排自己所需要的数据安全级别和性能要求。

二、隔离性

隔离性其实很好理解,即一个事务所做的写操作在它提交之前,对于其他事务是不可见的。那A事务修改了id=5的数据后B事务如何还能读到未修改数据呢,当然是记录下历史数据了,但是记录历史数据这个实现有两大流派,James Gray老爷子 的undo log流派和Michael Stonebraker老爷子的多版本派,二者在增删改上各有千秋。

所以说,MongoDB是没有undo log的,事务回滚是靠MVCC,不像MySQL,SQLServer等会有undo log 段。

WiredTiger的事务隔离是基于MVCC(读)和乐观锁(写)技术来实现,支持read uncommited、read commited和snapshot三种隔离级别,默认snapshot隔离级别。

2.1、读隔离

MongoDB并没有像MySQL一样提供指定隔离级别的机制,而是通过readConcern指定参数来实现:

db.test.find({_id : 5}).readConcern("snapshot").maxTimeMS(10000)

readConcern支持五种读隔离级别:local、available、majority、linearizable、snapshot。
我们先来看前 4 种,它们对一致性的承诺依次由弱到强,其中linearizable对应MySQL中的serializable,保证不会有幻读。

  1. local:读取本地所有可用且属于当前分片的数据,在这个级别下,发生重新选主时,已经读到的数据可能会被回滚掉,【默认设置】;
  2. available:读取本地所有可用的数据,在分片集群下也会读取不属于当前分片的数据,即孤儿文档

孤儿文档(orphaned document)是指在sharded cluster环境下,一些同时存在于不同shard上的document。我们知道,在mongodb sharded cluster中,分布在不同shard的数据子集是正交的,即理论上一个document只能出现在一个shard上,document与shard的映射关系维护在config server中。官方文档指出了可能产生orphaned document的情况:在chunk迁移的过程中,mongod实例异常宕机,导致迁移过程失败或者部分完成。文档中还指出,可以使用 cleanupOrphaned 来删除orphaned document,当然,在MongoDB 4.4之后其会在chunk迁移完成后自动清除orphaned document,不需要使用 cleanupOrphaned 来删除了。

  1. majority:读取「majority committed」的数据,可以保证读取的数据不会被回滚,但是并不能保证读到本地最新的数据。比如在Primary 节点上写x=5,立即在 Primary 节点读,虽然 x=5 已经是最新的已提交值,但是由于不是「majority committed」,因为还没有扩散到其他Secondary节点,所以当读操作使用 majority readConcern 时,只返回x=4。
  2. linearizable:线性一致性,既保证能读取到最新的数据(Recency Guarantee),也保证读到数据不会被回滚(Durability Guarantee);
  3. snapshot:「snapshot readConcern」是伴随着 4.0 中新出现的多文档事务( multi-document transaction)而设计的,只能用在显式开启的多文档事务中。而在 4.0 之前的版本中,对于一条读写操作,MongoDB 默认只支持单文档上的事务性语义(单行事务),前面提到的 4 种 readConcern level 正是为这些普通的读写操作(未显式开启多文档事务)而设计的。其与majority readConcern 比较相似,即,读取「majority committed」的数据,也可能读不到最新的已提交数据,但是其特殊性在于,当用在多文档事务中时,它承诺真正的一致性快照语义。

提到snapshot读隔离,不得不说MongoDB 5.0加的参数minSnapshotHistoryWindowInSeconds,可以然用户指定存储snapshot history的时间

2.2、写隔离

在MongoDB中,当多个事务同时修改同一个document数据时,就会有事务冲突,后面的事务检测到有其他的事务正在修改同一个document数据时,就会抛出WriteConflict写冲突异常,不会执行这个事务,这就是写隔离。
Mongodb的事务中的锁是乐观锁,不同于MySql悲观锁 ,其机制通过在文档中添加一个版本号字段来实现,当线程A要修改该文档时,先读取该文档的版本号并保存下来。当线程A提交修改后,会将版本号加1并更新到文档中。同时,MongoDB会检查文档中的版本号是否与线程A读取的版本号相同,如果不同,则说明其他线程在这之间已经修改了该文档,此时需要抛出异常重试或者返回错误信息

方案1:延长事务锁等待超时时间(ms,默认值是5)
官网解释如下:
0:such that if the transaction cannot acquire the required locks immediately, the transaction aborts.
-1:to use the operation specific timeout.
大于0:A number greater than 0 to wait the specified time to acquire the required locks. This can help obviate transaction aborts on momentary concurrent lock acquisitions, like fast-running metadata operations. However, this could possibly delay the abort of deadlocked transaction operations.

  1. 可以在线修改
db.adminCommand( { setParameter: 1, maxTransactionLockRequestTimeoutMillis: 36000000} );
  1. 启动的时候加入参数
mongod --setParameter maxTransactionLockRequestTimeoutMillis=36000000
  1. 在/etc/mongod.cnf加配置参数
setParameter:
  # 事务锁超时最长时间(毫秒)
  maxTransactionLockRequestTimeoutMillis: 36000000

方案2:代码中自行重试

三、参考文献

1]:WiredTiger存储引擎之四:WT工具编译与元数据文件剖析
2]:MongoDB的WiredTigerLAS.wt大小异常分析
3]:MongoDB 一致性模型设计与实现
4]:MongoDB丢数据问题的分析
5]:技术干货| MongoDB 事务原理
6]:Mongo内核:写冲突的产生和避免

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值