加入收藏 | 设为首页 | 会员中心 | 我要投稿 衡阳站长网 (https://www.0734zz.cn/)- 数据集成、设备管理、备份、数据加密、智能搜索!
当前位置: 首页 > 站长学院 > MySql教程 > 正文

认识MongoDB 4.0的新特性——事务(Transactions)

发布时间:2018-11-22 16:18:41 所属栏目:MySql教程 来源:创宇前端
导读:前言 相信使用过主流的关系型数据库的朋友对事务(Transactions)不会太陌生,它可以让我们把对多张表的多次数据库操作整合为一次原子操作,这在高并发场景下可以保证多个数据操作之间的互不干扰;并且一旦在这些操作过程任一环节中出现了错误,事务会中止并
副标题[/!--empirenews.page--]

前言

相信使用过主流的关系型数据库的朋友对“事务(Transactions)”不会太陌生,它可以让我们把对多张表的多次数据库操作整合为一次原子操作,这在高并发场景下可以保证多个数据操作之间的互不干扰;并且一旦在这些操作过程任一环节中出现了错误,事务会中止并且让数据回滚,这使得同时在多张表中修改数据的时候保证了数据的一致性。

以前 MongoDB 是不支持事务的,因此开发者在需要用到事务的时候,不得不借用其他工具,在业务代码层面去弥补数据库的不足。随着 4.0 版本的发布,MongoDB 也为我们带来了原生的事务操作,下面就让我们一起来认识它,并通过简单的例子了解如何去使用。

认识MongoDB 4.0的新特性——事务(Transactions)

介绍

事务和副本集(Replica Sets)

副本集是 MongoDB 的一种主副节点架构,它使数据得到最大的可用性,避免单点故障引起的整个服务不能访问的情况的发生。目前 MongoDB 的多表事务操作仅支持在副本集上运行,想要在本地环境安装运行副本集可以借助一个工具包——run-rs,以下的文章中有详细的使用说明:

https://thecodebarbarian.com/...

事务和会话(Sessions)

事务和会话(Sessions)关联,一个会话同一时刻只能开启一个事务操作,当一个会话断开,这个会话中的事务也会结束。

事务中的函数

  • Session.startTransaction()

在当前会话中开始一次事务,事务开启后就可以开始进行数据操作。在事务中执行的数据操作是对外隔离的,也就是说事务中的操作是原子性的。

  • Session.commitTransaction()

提交事务,将事务中对数据的修改进行保存,然后结束当前事务,一次事务在提交之前的数据操作对外都是不可见的。

  • Session.abortTransaction()

中止当前的事务,并将事务中执行过的数据修改回滚。

重试

当事务运行中报错,catch 到的错误对象中会包含一个属性名为 errorLabels 的数组,当这个数组中包含以下2个元素的时候,代表我们可以重新发起相应的事务操作。

  • TransientTransactionError:出现在事务开启以及随后的数据操作阶段
  • UnknownTransactionCommitResult:出现在提交事务阶段

示例

经过上面的铺垫,你是不是已经迫不及待想知道究竟应该怎么写代码去完成一次完整的事务操作?下面我们就简单写一个例子:

场景描述: 假设一个交易系统中有2张表——记录商品的名称、库存数量等信息的表 commodities,和记录订单的表 orders。当用户下单的时候,首先要找到 commodities 表中对应的商品,判断库存数量是否满足该笔订单的需求,是的话则减去相应的值,然后在 orders 表中插入一条订单数据。在高并发场景下,可能在查询库存数量和减少库存的过程中,又收到了一次新的创建订单请求,这个时候可能就会出问题,因为新的请求在查询库存的时候,上一次操作还未完成减少库存的操作,这个时候查询到的库存数量可能是充足的,于是开始执行后续的操作,实际上可能上一次操作减少了库存后,库存的数量就已经不足了,于是新的下单请求可能就会导致实际创建的订单数量超过库存数量。

以往要解决这个问题,我们可以用给商品数据“加锁”的方式,比如基于 Redis 的各种锁,同一时刻只允许一个订单操作一个商品数据,这种方案能解决问题,缺点就是代码更复杂了,并且性能会比较低。如果用数据库事务的方式就可以简洁很多:

commodities 表数据(stock 为库存):

  1. { "_id" : ObjectId("5af0776263426f87dd69319a"), "name" : "灭霸原味手套", "stock" : 5 } 
  2. { "_id" : ObjectId("5af0776263426f87dd693198"), "name" : "雷神专用铁锤", "stock" : 2 } 

orders 表数据:

  1. { "_id" : ObjectId("5af07daa051d92f02462644c"), "commodity": ObjectId("5af0776263426f87dd69319a"), "amount": 2 } 
  2. { "_id" : ObjectId("5af07daa051d92f02462644b"), "commodity": ObjectId("5af0776263426f87dd693198"), "amount": 3 } 

通过一次事务完成创建订单操作(mongo Shell):

  1. // 执行 txnFunc 并且在遇到 TransientTransactionError 的时候重试 
  2. function runTransactionWithRetry(txnFunc, session) { 
  3.   while (true) { 
  4.     try { 
  5.       txnFunc(session); // 执行事务 
  6.       break; 
  7.     } catch (error) { 
  8.       if ( 
  9.         error.hasOwnProperty('errorLabels') && 
  10.         error.errorLabels.includes('TransientTransactionError') 
  11.       ) { 
  12.         print('TransientTransactionError, retrying transaction ...'); 
  13.         continue; 
  14.       } else { 
  15.         throw error; 
  16.       } 
  17.     } 
  18.   } 
  19.  
  20. // 提交事务并且在遇到 UnknownTransactionCommitResult 的时候重试 
  21. function commitWithRetry(session) { 
  22.   while (true) { 
  23.     try { 
  24.       session.commitTransaction(); 
  25.       print('Transaction committed.'); 
  26.       break; 
  27.     } catch (error) { 
  28.       if ( 
  29.         error.hasOwnProperty('errorLabels') && 
  30.         error.errorLabels.includes('UnknownTransactionCommitResult') 
  31.       ) { 
  32.         print('UnknownTransactionCommitResult, retrying commit operation ...'); 
  33.         continue; 
  34.       } else { 
  35.         print('Error during commit ...'); 
  36.         throw error; 
  37.       } 
  38.     } 
  39.   } 
  40.  
  41. // 在一次事务中完成创建订单操作 
  42. function createOrder(session) { 
  43.   var commoditiesCollection = session.getDatabase('mall').commodities; 
  44.   var ordersCollection = session.getDatabase('mall').orders; 
  45.   // 假设该笔订单中商品的数量 
  46.   var orderAmount = 3; 
  47.   // 假设商品的ID 
  48.   var commodityID = ObjectId('5af0776263426f87dd69319a'); 
  49.  
  50.   session.startTransaction({ 
  51.     readConcern: { level: 'snapshot' }, 
  52.     writeConcern: { w: 'majority' }, 
  53.   }); 
  54.  
  55.   try { 
  56.     var { stock } = commoditiesCollection.findOne({ _id: commodityID }); 
  57.     if (stock < orderAmount) { 
  58.       print('Stock is not enough'); 
  59.       session.abortTransaction(); 
  60.       throw new Error('Stock is not enough'); 
  61.     } 
  62.     commoditiesCollection.updateOne( 
  63.       { _id: commodityID }, 
  64.       { $inc: { stock: -orderAmount } } 
  65.     ); 
  66.     ordersCollection.insertOne({ 
  67.       commodity: commodityID, 
  68.       amount: orderAmount, 
  69.     }); 
  70.   } catch (error) { 
  71.     print('Caught exception during transaction, aborting.'); 
  72.     session.abortTransaction(); 
  73.     throw error; 
  74.   } 
  75.  
  76.   commitWithRetry(session); 
  77.  
  78. // 发起一次会话 
  79. var session = db.getMongo().startSession({ readPreference: { mode: 'primary' } }); 
  80.  
  81. try { 
  82.   runTransactionWithRetry(createOrder, session); 
  83. } catch (error) { 
  84.   // 错误处理 
  85. } finally { 
  86.   session.endSession(); 

(编辑:衡阳站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读