MongoDB学习6:MongoDB的事务处理
<h2 id="1mongodb的写操作事务">1.MongoDB的写操作事务</h2><h3 id="写入策略-writeconcern">写入策略 writeConcern</h3>
<p>语法:<code>db.collection.insert({x: 1}, {writeConcern: {w: 1}})</code></p>
<h4 id="什么是writeconcern">什么是writeConcern?</h4>
<p>writeConcern决定一个写操作落到多少个节点上才算成功,这决定了MongoDB是否成功写入数据。writeConcern的取值有以下:</p>
<ul>
<li>0:发起写入操作,不关心是否成功(适用于性能要求高,但不关注正确性的场景)</li>
<li>1-集群最大节点数:写操作需要被复制到指定节点数才算成功</li>
<li>majority:写操作需要被复制到大多数节点上才算成功(适用于对数据安全性要求比较高的场景,该选项会降低写入性能<br>
)</li>
<li>all:复制到全部节点上才算成功</li>
</ul>
<p>发起写操作的程序将阻塞到写操作到达指定的节点数为止</p>
<h4 id="writeconcern的行为">writeConcern的行为</h4>
<h5 id="以3节点复制集为例不做任何特定设置">以3节点复制集为例:不做任何特定设置</h5>
<p><img src="https://img2020.cnblogs.com/blog/579926/202008/579926-20200815203822325-834256646.png"><br>
上图表示一个写操作进入后,直接写入主节点成功就返回了,后台会异步复制到从节点secondary1和secondary2。但假如数据刚写入主节点后,从节点还没有复制数据,主节点就宕机了,此时MongoDB就可能出现丢失数据的问题,那么如何解决呢?</p>
<h5 id="参数-wmajority">参数 w:"majority"</h5>
<p><img src="https://img2020.cnblogs.com/blog/579926/202008/579926-20200815204359049-122522768.png"><br>
majority表示数据写入大多数(超半数)节点后才算成功。此时如果主节点再发生宕机情况,那么从节点secondary1就会被选举为新的主节点,数据也没有丢失</p>
<h5 id="参数-wall">参数 w:"all"</h5>
<p><img src="https://img2020.cnblogs.com/blog/579926/202008/579926-20200815204802813-2082755501.png"><br>
all表示确认全部节点写入成功后才返回。这是一种最安全的写法,数据绝对不会丢失,但是如果有一个节点故障,那么就会发生阻塞一直等待</p>
<h5 id="参数-jtrue">参数 j:true</h5>
<p>j表示写入操作的journal持久化后才向客户端确认,取值有:</p>
<ul>
<li>true:写操作落到 journal 文件中才算成功</li>
<li>false:写操作到达内存即算成功<br>
<img src="https://img2020.cnblogs.com/blog/579926/202008/579926-20200815205627072-1941254050.png"></li>
</ul>
<h4 id="参数-wtimeout-写入超时时间仅w的值大于1时有效">参数 wtimeout: 写入超时时间,仅w的值大于1时有效</h4>
<ul>
<li>当指定{w: }时,数据需要成功写入n个节点才算成功,如果写入过程中有节点故障,可能导致这个条件一直不能满足,从而一直不能向客户端发送确认结果,针对这种情况,客户端可设置wtimeout选项来指定超时时间,当写入过程持续超过该时间仍未结束,则认为写入失败</li>
</ul>
<h4 id="writeconcern测试">writeConcern测试</h4>
<p>以下测试在3个节点环境中</p>
<pre><code>db.test.insert({count:1},{writeConcern:{w:"majority"}})
db.test.insert({count:1},{writeConcern:{w:3}})
db.test.insert({count:1},{writeConcern:{w:4}}) # 报错:Not enough data-bearing nodes
db.test.insert({count:1},{writeConcern:{w:3,wtimeout:3000}})# 超过3s未响应则不再等待直接返回
</code></pre>
<p>slaveDelay:设置节点延迟时间(单位:s),延迟多久才会同步数据</p>
<pre><code>var conf = rs.conf
conf.members.salveDelay = 10#设置节点3延迟10秒
conf.members.priority=0#设置了延迟的节点不能参与选举
</code></pre>
<h4 id="注意事项">注意事项</h4>
<ul>
<li>虽然多半数的 writeConcern都是安全的,但通常只会设置 majority,因为这是等待写入延迟时间最短的选择</li>
<li>不要设置 writeConcern 等于总节点数,因为一旦有一个节点故障,所有写操作都会失败</li>
<li>writeConcern 虽然会增加写操作的延迟时间,但并不会显著增加集群的压力,因此无论是否等待,写操作最终都会复制到所有节点上。但设置 writeConcern只是让写操作等待复制后在返回而已</li>
<li>应对重要数据(订单、金融有关的)应用 {w:"majority"},普通数据(日志)可以应用{w:1}以确保最佳性能</li>
</ul>
<h2 id="2mongodb的读操作事务">2.MongoDB的读操作事务</h2>
<p>在读取数据的过程中我们需要关注以下问题:</p>
<ul>
<li>从哪里读?关注数据节点的位置(由readPerference解决)</li>
<li>什么样的数据可以读?关注数据的隔离性(由readConcern解决)</li>
</ul>
<h3 id="readperference">readPerference:</h3>
<p>使用:<code>db.collection.find({}).readPerf("secondary")</code></p>
<h5 id="readperference决定使用哪一个节点来满足正在发起的读请求可选值包括">readPerference决定使用哪一个节点来满足正在发起的读请求,可选值包括:</h5>
<ul>
<li>primary(默认):只选择主节点,保证每次读到的数据都是最新的</li>
<li>primaryPerferred:优先选择主节点,如果不可用则选择从节点</li>
<li>secondary:只选择从节点</li>
<li>secondaryPerferred:优先选择从节点,如果从节点不可用则选择主节点</li>
<li>nearest:选择最近的节点,针对多区域部署的情况</li>
</ul>
<h5 id="readperference使用场景举例">readPerference使用场景举例:</h5>
<ul>
<li>用户下单后马上跳转到订单详情页--primary/primaryPerferred,因为此时从节点可能还没复制到新的订单数据</li>
<li>用户查询自己下过的订单--secondary/secondaryPerferred,查询历史订单对时效性通常没有太高要求</li>
<li>生成报表--secondary,报表对时效性要求不高,但资源需求大,可以在从节点单独处理,避免影响主节点操作</li>
<li>将用户上传的图片分发到全世界,让各地用户就近读取--nearest,每个地区的应用选择最近的节点读取数据</li>
</ul>
<h5 id="readperference与tag">readPerference与Tag</h5>
<p>readPreference只能控制使用一类节点。Tag则可以将节点选择控制到一个或几个具体的节点,有以下场景:</p>
<ul>
<li>一个5个节点的复制集</li>
<li>3个节点硬件较好,专用于服务线上用户</li>
<li>2个节点硬件较差,专用于生成包报表</li>
</ul>
<p>可以使用Tag来达到这样的控制目的:</p>
<ul>
<li>为3个较好的节点打上</li>
<li>为2个较差的节点打上</li>
<li>在线应用读取时指定online,报表读取时指定analyse</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/579926/202008/579926-20200815214359254-1516485675.png"></p>
<h5 id="readperference配置">readPerference配置:</h5>
<ul>
<li>通过MongoDB的连接字符串:mongodb://host:27017,host2:27017,host3:27017/?replicaSet=rs&readPerferende=secondary</li>
<li>通过MongoDB驱动程序API:MongoCollection.withReadPerference(ReadPerference readPerf)</li>
<li>Mongo Shell:db.collection.find({}).readPerf("secondary")</li>
</ul>
<h5 id="readperference注意事项">readPerference注意事项:</h5>
<ul>
<li>指定readPerference时也应该注意高可用问题。例如将 readPerference 指定primary,则发生故障转移不存在primary期间将没有节点可读。如果业务允许,则应选择primaryPerference</li>
<li>使用Tag时也会遇到同样的问题,如果只有一个节点拥有一个特定的Tag,则这个节点宕机时将无节点可读。这在有时候是期望的结果,有时候不是:
<ul>
<li>如果报表使用的节点失效,即使不生成报表,通常也不希望将报表负载转移到其他节点上,此时只有一个节点有报表Tag是合理的选择</li>
<li>如果线上节点失效,通常希望有替代节点,则应该保持多个节点有同样的Tag</li>
</ul>
</li>
<li>Tag有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应为0</li>
</ul>
<h3 id="readconcern">readConcern:</h3>
<p>使用:<code>db.test.find().readConcern("majority")</code></p>
<h5 id="在readperference选择了指定节点后readconcern决定这个节点上的数据哪些是可读的类似于关系型数据库的隔离级别包括">在readPerference选择了指定节点后,readConcern决定这个节点上的数据哪些是可读的,类似于关系型数据库的隔离级别,包括:</h5>
<ul>
<li>available:读取所有可用的数据</li>
<li>local(默认):读取所有可用且属于当前分片的数据</li>
<li>majority:读取再大多数节点上提交完成的数据</li>
<li>linearizable:可线性化读取文档</li>
<li>snapshot:读取最近快照中的数据</li>
</ul>
<h5 id="readconcernlocal和available">readConcern:local和available</h5>
<p>在复制集中local和available是没有区别的,两者的区别主要体现在分片集上,有以下场景:</p>
<ul>
<li>一个chunk x 正在从shard1向shard2迁移</li>
<li>整个迁移过程中chunk x 中的部分数据会在shard1和shard2中同时存在,但源分片shard1仍然是chunk x的负责方
<ul>
<li>所有对chunk x的读操作仍然进入shard1</li>
<li>config中记录的信息chunk x仍然属于shard1</li>
</ul>
</li>
<li>此时如果度shard2,则会体现出local和available的区别:
<ul>
<li>local:只取应该由shard2负责的数据(不包括x)</li>
<li>available:shard2上有什么就读什么(包括x)</li>
</ul>
</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/579926/202008/579926-20200815220516853-716605928.png"></p>
<h5 id="readconcernmajority">readConcern:majority</h5>
<p>只读取大多数数据节点上都提交了的数据,考虑如下场景:</p>
<ul>
<li>集合中原有文档</li>
<li>将x值更新为1<br>
<img src="https://img2020.cnblogs.com/blog/579926/202008/579926-20200815220826332-2098216809.png"><br>
如果在各节点上应用 {readConcern:"majority"}来读取数据:<br>
<img src="https://img2020.cnblogs.com/blog/579926/202008/579926-20200815220936917-37807009.png"></li>
</ul>
<h5 id="readconcernmajority与脏读">readConcern:majority与脏读</h5>
<p>MongoDB中的回滚:</p>
<ul>
<li>写操作到达大多数节点之前都是不安全的,一旦主节点崩溃,而从节点还没复制到该次操作,刚才的写操作就丢失了</li>
<li>把一次写操作视为一个事务,从事务的角度来看,可以认为事务被回滚了。<br>
所以从分布式系统的角度来看,事务的提交被提升到了分布式集群的多个节点级别的“提交”,而不是单个节点的“提交”</li>
</ul>
<p>在可能发生回滚的前提下考虑脏读问题:</p>
<ul>
<li>如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生脏读问题,使用 {readConcern:"majority"}可以有效避免脏读</li>
</ul>
<h5 id="readconcern实现安全的读写分离">readConcern:实现安全的读写分离</h5>
<p>有以下场景</p>
<ul>
<li>向主节点写入一条数据</li>
<li>立即从从节点读取这条数据</li>
</ul>
<p>如何保证自己能够读取到刚刚写入的数据?<br>
下述方式有可能读不到刚写入的订单:</p>
<pre><code>db.order.insert({id:100,sku:"kite",q:1})
db.order.find({id:100}).readPerf("secondary")
</code></pre>
<p>使用writeConcern + readConcern majority来解决:</p>
<pre><code>db.order.insert({id:100,sku:"kite",q:1},{writeConcern:{w:"majority"}})
db.order.find({id:100}).readPerf("secondary").readConcern("majority")
</code></pre>
<h5 id="readconcernlinearizable">readConcern:linearizable</h5>
<p>只读取大多数节点确认过的数据。和majority最大差别是保证绝对的操作线性顺序-在写操作自然时间后面发生的读,一定可以读到之前的写</p>
<ul>
<li>只对读取单个文档时有效</li>
<li>可能导致非常慢的读,因此总是建议配合使用 maxTimeMS</li>
</ul>
<h5 id="readconcernsnapshot">readConcern:snapshot</h5>
<p>只在多文档事务中生效。将一个事务的 readConcern设置为snapshot将保证在事务中的读:</p>
<ul>
<li>不出现脏读</li>
<li>不出现不可重复读</li>
<li>不出现幻读</li>
</ul>
<p>因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放</p>
<h2 id="3mongodb的多文档事务">3.MongoDB的多文档事务</h2>
<p>对事务的使用原则应该是:能不用尽量不用<br>
通过合理的设计文档模型,可以避免大部分使用事务的必要性,因为事务=锁,节点协调需要额外开销,影响性能</p>
<h3 id="mongodb-acid多文档事务支持">MongoDB ACID多文档事务支持</h3>
<table>
<thead>
<tr>
<th>事务属性</th>
<th>支持程度</th>
</tr>
</thead>
<tbody>
<tr>
<td>Atomocity 原子性</td>
<td>单表单文档:1.x就支持 <br> 复制集多表多行:4.0复制集 <br> 分片集群多表多行:4.2</td>
</tr>
<tr>
<td>Consistency 一致性</td>
<td>writeCOncern、readConcern(3.2)</td>
</tr>
<tr>
<td>Isolation 隔离性</td>
<td>readConcern(3.2)</td>
</tr>
<tr>
<td>Durability 持久性</td>
<td>Journal and Replication</td>
</tr>
</tbody>
</table>
<h3 id="使用方法">使用方法</h3>
<p>MongoDB多文档事务使用方式和关系型数据库非常相似,以java为例</p>
<pre><code class="language-java">try(ClientSession clientSession = client.startSession()){
clientSession.startTransaction();
collection.insertOne(clientSession,docOne);
collection.insertOne(clientSession,docTwo);
clientSession.commitTransaction();
}
</code></pre>
<h3 id="事务的隔离级别">事务的隔离级别</h3>
<ul>
<li>事务完成前,<strong>事务外的操作</strong>对该事务所做的修改不可访问</li>
<li>如果事务内使用{readConcern:"snapshot"},则可以达到可重复度 Repeatable Read</li>
</ul>
<h3 id="事务写机制">事务写机制</h3>
<p>MongoDB的事务错误处理机制不同于关系型数据库</p>
<ul>
<li>当一个事务开始后,如果事务要修改的文档在事务外部被修改过,则事务修改这个文档时会出发Abort错误,因为此时修改冲突了,这种情况下,只需要简单的重做事务就可以了</li>
<li>如果一个事务已经开始修改一个文档,在事务以外尝试修改同一个文档,则事务以外的修改会等待事务完成才能继续进行</li>
</ul>
<h3 id="注意事项-1">注意事项</h3>
<ul>
<li>可以实现和关系型数据库类似的事务场景</li>
<li>必须使用与MongoDB4.2兼容的驱动</li>
<li>事务默认必须在60s(可调)内完成,否则将被取消</li>
<li>涉及事务的分片不能使用仲裁节点</li>
<li>事务会影响chunk迁移效率。正在迁移的chunk也可能造成事务提交失败(重试即可)</li>
<li>多文档事务中的读操作必须使用主节点读</li>
<li>readConcern只应该在事务级别设置,不能设置在每次读写操作上</li>
</ul><br><br>
来源:https://www.cnblogs.com/xiaoqingtian/p/13510898.html
頁:
[1]