赞
踩
前面文章介绍了MongoDB入门介绍与实战-CSDN博客,今天就分享下MongoDB的事务。
MongoDB官网:MongoDB:应用程序数据平台 | MongoDB
事务(transaction)是传统数据库所具备的一项基本能力,其根本目的是为数据的可靠性与一致性提供保障。而在通常的实现中,事务包含了一个系列的数据库读写操作,这些操作要么全部完成,要么全部撤销。例如,在电子商城场景中,当顾客下单购买某件商品时,除了生成订单,还应该同时扣减商品的库存,这些操作应该被作为一个整体的执行单元进行处理,否则就会产生不一致的情况。
数据库事务需要包含4个基本特性,即常说的ACID,具体如下:
在MongoDB中,对单个文档的操作是原子的。由于可以在单个文档结构中使用内嵌文档和数组来获得数据之间的关系,而不必跨多个文档和集合进行范式化,所以这种单文档原子性避免了许多实际场景中对多文档事务的需求。
对于那些需要对多个文档(在单个或多个集合中)进行原子性读写的场景,MongoDB支持多文档事务。而使用分布式事务,事务可以跨多个操作、集合、数据库、文档和分片使用。
MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制地使用它。相反,对事务的使用原则应该是:能不用尽量不用。 通过合理地设计文档模型,可以规避绝大部分使用事务的必要性。
使用事务的原则
MongoDB对事务支持
事务属性 | 支持程度 |
Atomocity 原子性 | 单表单文档 : 1.x 就支持 复制集多表多行:4.0 分片集群多表多行:4.2 |
Consistency 一致性 | writeConcern, readConcern (3.2) |
Isolation 隔离性 | readConcern (3.2) |
Durability 持久性 | Journal and Replication |
使用方法
MongoDB 多文档事务的使用方式与关系数据库非常相似:
- try (ClientSession clientSession = client.startSession()) {
- clientSession.startTransaction();
- collection.insertOne(clientSession, docOne);
- collection.insertOne(clientSession, docTwo);
- clientSession.commitTransaction();
- }
writeConcern
Write Concern — MongoDB Manual
writeConcern 决定一个写操作落到多少个节点上才算成功。MongoDB支持客户端灵活配置写入策略(writeConcern),以满足不同场景的需求。
语法格式:
{ w: <value>, j: <boolean>, wtimeout: <number> }
思考:对于5个节点的复制集来说,写操作落到多少个节点上才算是安全的?
1 2 3 4 5 majority
测试
包含延迟节点的3节点pss复制集
- db.user.insertOne({name:"李四"},{writeConcern:{w:"majority"}})
- # 等待延迟节点写入数据后才会响应
- db.user.insertOne({name:"王五"},{writeConcern:{w:3}})
- # 超时写入失败
- db.user.insertOne({name:"小明"},{writeConcern:{w:3,wtimeout:3000}})
注意事项
在读取数据的过程中我们需要关注以下两个问题:
第一个问题是是由 readPreference 来解决,第二个问题则是由 readConcern 来解决
readPreference
readPreference决定使用哪一个节点来满足正在发起的读请求。可选值包括:
合理的 ReadPreference 可以极大地扩展复制集的读性能,降低访问延迟。
readPreference 场景举例
readPreference 配置
通过 MongoDB 的连接串参数:
- mongodb://host1:27107,host2:27107,host3:27017/?replicaSet=rs0&readPre
- ference=secondary
通过 MongoDB 驱动程序 API:
MongoCollection.withReadPreference(ReadPreference readPref)
Mongo Shell:
db.collection.find().readPref( "secondary" )
从节点读测试
1. 主节点写入{count:1} , 观察该条数据在各个节点均可见
- # mongo --host rs0/localhost:28017
- rs0:PRIMARY> db.user.insert({count:1})
在primary节点中调用readPref("secondary")查询从节点用直连方式(mongo localhost:28017)会查到数据,需要通过mongo --host rs0/localhost:28017方式连接复制集,参考: Loading...
2. 在两个从节点分别执行 db.fsyncLock() 来锁定写入(同步)
- # mongo localhost:28018
- rs0:SECONDARY> rs.secondaryOk()
- rs0:SECONDARY> db.fsyncLock()
3. 主节点写入 {count:2}
- rs0:PRIMARY> db.user.insert({count:2})
- rs0:PRIMARY> db.user.find()
- rs0:PRIMARY> db.user.find().readPref("secondary")
- rs0:SECONDARY> db.user.find()
4. 解除从节点锁定 db.fsyncUnlock()
rs0:SECONDARY> db.fsyncUnlock()
5. 主节点中查从节点数据
rs0:PRIMARY> db.user.find().readPref("secondary")
扩展:Tag
readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制到一个或几个节点。考虑以下场景:
可以使用 Tag 来达到这样的控制目的:
- # 为复制集节点添加标签
- conf = rs.conf()
- conf.members[1].tags = { purpose: "online"}
- conf.members[4].tags = { purpose: "analyse"}
- rs.reconfig(conf)
-
- #查询
- db.collection.find({}).readPref( "secondary", [ {purpose: "analyse"} ] )
注意事项
readConcern
在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些是可读的,类似于关系数据库的隔离级别。可选值包括:
readConcern: local 和 available
在复制集中 local 和 available 是没有区别的,两者的区别主要体现在分片集上。
考虑以下场景:
注意事项:
readConcern: majority 与脏读
MongoDB 中的回滚:
所以从分布式系统的角度来看,事务的提交被提升到了分布式集群的多个节点级别的“提交”,而不再是单个节点上的“提交”。
在可能发生回滚的前提下考虑脏读问题:
回滚了,则发生了脏读问题;
使用 {readConcern: "majority"} 可以有效避免脏读
如何安全的读写分离
考虑如下场景:
思考: 如何保证自己能够读到刚刚写入的数据?
下述方式有可能读不到刚写入的订单
- db.orders.insert({oid:101,sku:"kite",q:1})
- db.orders.find({oid:101}).readPref("secondary")
使用writeConcern+readConcern majority来解决
- db.orders.insert({oid:101,sku:"kite",q:1},{writeConcern:{w:"majority"}})
- db.orders.find({oid:101}).readPref("secondary").readConcern("majority")
readConcern: linearizable
只读取大多数节点确认过的数据。和 majority 最大差别是保证绝对的操作线性顺序
readConcern: snapshot
{readConcern: “snapshot”} 只在多文档事务中生效。将一个事务的 readConcern 设置为 snapshot,将保证在事务中的读:
因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放。
小结
事务隔离级别
- db.tx.insertMany([{ x: 1 }, { x: 2 }])
- var session = db.getMongo().startSession()
- # 开启事务
- session.startTransaction()
-
- var coll = session.getDatabase("test").getCollection("tx")
- #事务内修改 {x:1, y:1}
- coll.updateOne({x: 1}, {$set: {y: 1}})
- #事务内查询 {x:1}
- coll.findOne({x: 1}) //{x:1, y:1}
-
- #事务外查询 {x:1}
- db.tx.findOne({x: 1}) //{x:1}
-
- #提交事务
- session.commitTransaction()
-
- # 或者回滚事务
- session.abortTransaction()
- var session = db.getMongo().startSession()
- session.startTransaction({ readConcern: {level: "snapshot"}, writeConcern: {w: "majority"}})
-
- var coll = session.getDatabase('test').getCollection("tx")
-
- coll.findOne({x: 1})
- db.tx.updateOne({x: 1}, {$set: {y: 1}})
- db.tx.findOne({x: 1})
- coll.findOne({x: 1})
-
- session.abortTransaction()
在执行事务的过程中,如果操作太多,或者存在一些长时间的等待,则可能会产生如下异常:
原因在于,默认情况下MongoDB会为每个事务设置1分钟的超时时间,如果在该时间内没有提交,就会强制将其终止。该超时时间可以通过transactionLifetimeLimitSecond变量设定。
MongoDB 的事务错误处理机制不同于关系数据库:
写冲突测试
开3个 mongo shell 均执行下述语句
- var session = db.getMongo().startSession()
- session.startTransaction()
- var coll = session.getDatabase('test').getCollection("tx")
窗口1: 正常结束
coll.updateOne({x: 1}, {$set: {y: 1}})
窗口2: 异常 – 解决方案:重启事务
coll.updateOne({x: 1}, {$set: {y: 2}})
窗口3:事务外更新,需等待
db.tx.updateOne({x: 1}, {$set: {y: 3}})
更多文章
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。